基础dp 背包问题详解(01,完全,多重,分组,多消耗,判满,路径)

越基础越能玩…


背包

dp[i]表示i大小的背包可以装下的最大价值,对于i物品大小为siz[i],价值为v[i],枚举背包大小j
递推方程为 dp[j]=max(dp[j],dp[j-siz[i]+v[i])

01包,完全包

首先,说01和完全背包,这两个都是很基础的背包,他们两个的区别在于是由当前状态转移而来还是由上一个状态转移而来,由当前状态转移而来代表可是选无数个,就是完全背包,而由上一个状态而来就是只能选一个了,就是01背包,这个如果是二维的,你怎么选无所谓,但是如果是一维的话,就必须从后向前选,因为我们要用前一个状态,而在判断dp[j-v[i]]的时候,这个正好就是前一个的状态,如果正着选,那用的正好就是当前的状态,也就正好就是完全背包。

01包:

void ZeroOne_Pack(int cost,int weight,int n)  
{  
    for(int i=n; i>=cost; i--)  
        dp[i] = max(dp[i],dp[i-cost] + weight);  
} 

完全包:

void Complete_Pack(int cost,int weight,int n)  
{  
    for(int i=cost; i<=n; i++)  
        dp[i] = max(dp[i],dp[i-cost] + weight);  
} 

多重包

多重包就是每个物品都有一个数量,我们用二进制优化,就对每件物品得到几个块(2^i绑在一起的原物品)
当然,如果你对一件物品取所有的数量的情况下发现塞不进背包了,就说明相当于无限数量,做完全包就行

int Multi_Pack(int c[],int w[],int num[],int n,int m)  
{  
    memset(dp,0,sizeof(dp));  
    for(int i=1; i<=n; i++)  
    {  
        if(num[i]*c[i] > m)  
            Complete_Pack(c[i],w[i],m);  
        else  
        {  
            int k = 1;  
            while(k < num[i])  
            {  
                ZeroOne_Pack(k*c[i],k*w[i],m);  
                num[i] -= k;  
                k <<= 1;  
            }  
            ZeroOne_Pack(num[i]*c[i],num[i]*w[i],m);  
        }  
    }  
    return dp[m];  
} 

改进完全包

对每件物品做多重包的二进制优化,在数量比较大的情况下可以节省很多时间

void Complete_Pack(int i){
    nu[i]=big/val[i]+1;
    int b=1;
    while(2*b-1<=nu[i]){
        ZeroOne_Pack(i,b);b<<=1;
    }
    b>>=1;
    if(nu[i]-2*b+1>0)ZeroOne_Pack(i,nu[i]-2*b+1);
} 

多消耗背包

一般的背包就是一维,代表背包的大小,多消耗相当于多了一维的消耗

void ZeroOne_Pack(int cost1,int cost2,int weight,int n)  
{  
    for(int i=n; i>=cost; i--)  
        for(int j=m; j>=cost; j--)  
            dp[i][j] = max(dp[i][j],dp[i-cost1][j-cost2] + weight);  
} 

分组背包

对于所有物品分成若干组,每组只能取一个物品,我们只需要对每一组遍历,先对背包大小遍历,在对组内的物品遍历就可以避免每组物品的重复(因为由组内的一件物品转移来的状态不会被另一件物品转移)

for(int i=1;i<=numbag;i++){
    for(int j=maxsiz;j>=0;j++){
        for(int k=1;k<=num;k++){
            if(j>=siz[k]&&dp[j]<dp[j-siz[k]]+v[k])
                dp[j]=dp[j-siz[k]]+v[k]
        }
    }
}

判断是否装满

dp就是状态的转换,我们可以对所有初始状态赋初值为-1(dp[0]=0),表示这个状态不能被转移,这样只有可以转移的包才是塞满的包,我们判断是否塞满一个大小为maxsiz的包就判断dp[maxsiz]是否等于-1

当然,也可以赋初值为-inf,保证除了由0状态转移以外是得不到正数的就可以通过dp[maxsiz]的正负来判断

for(int i=1;i<=maxsiz;i++)dp[i]=-inf;
for(int i=1;i=n;i++){
    for(int j=maxsiz;j>=siz[i];j--){
        dp[j]=max(dp[j],dp[j-siz[i]]+v[i]);
    }
}

路径或方案求解

用一个path[]记录一下转移时的路径(path[i]==j -> i大小的包由i-siz[j]大小的包加上siz[i]转移 ),在输出任意方案的情况下只需要对终点往后推就可以得到这条路径

for(int i=1;i<=n;i++){
    for(int j=maxsiz;j>=siz[i];j--){
        if(dp[j-siz[i]]<0)continue;
        if(dp[j]<dp[j-siz[i]]+v[i])
            dp[j]=dp[j-siz[i]]+v[i],path[j]=i;
    }
}
int pos=path[size];
while(1){
    printf("%d\n",pos);
    if(size-siz[pos]==0)break;
    pos=path[size-siz[pos]];
}

猜你喜欢

转载自blog.csdn.net/jk_chen_acmer/article/details/81542948