动态规划(0-1、完全、多重、分组、超大背包问题)

个人整理的算法笔记:参考SDU程序设计思维与实践Week11-动态规划(二)+《挑战程序设计竞赛》

0-1背包问题

  • 有 N 件物品和一个容量为 V 的背包。 第 i 件物品体积是 Wi,价值是Vi。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大 。
  • 特点:每种物品仅有一件,可以选择放或不放 (对应 1 或 0)。
  • 状态转移方程:
    • f ( i , j ) f(i,j) f(i,j) 表示仅考虑前 i i i 件物品,放入一个容量为 j j j 的背包可以获得的最大价值
    • f ( i , j ) = m a x ( f ( i − 1 , j ) , f ( i − 1 , j − w i ) + v i ) f(i,j) = max( f(i-1,j) , f(i-1,j-w_i)+v_i) f(i,j)=max(f(i1,j),f(i1,jwi)+vi)
  • 如果背包中的限制为容量恰好为 j j j ,则代码为
//初始化
for(int i=1;i<=V;i++)
	f[0][i]=-inf;
f[0][0]=0,ans=0;
//状态转移
for(int i=1;i<=N;i++){
	for(int j=0;j<=V;j++){
		f[i][j]=f[i-1][j];
		if(j-w[i]>=0)
			f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
	}
}
//获得答案
for(int i=0;i<=V;i++)
	ans=max(ans,f[N][i]);
  • 如果背包中的限制为容量至多为 j j j,代码为
//初始化
for(int i=0;i<=V;i++)
	f[0][i]=0;
//状态转移
for(int i=1;i<=N;i++){
	for(int j=0;j<=V;j++){
		f[i][j]=f[i-1][j];
		if(j-w[i]>=0)
			f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
	}
}
//获得答案
ans=f[N][V];
  • 滚动数组优化
  • 关键点:逆序
memset(f,0,sizeof(f));
for(int i=1;i<=N;i++)
	for(int j=V;j>=w[i];j--)
		f[j]=max(f[j],f[j-w[i]]+v[i]);
ans=f[V];
  • 复杂度
  • 时间复杂度 —— O ( N × V ) O(N×V) O(N×V)
  • 空间复杂度 —— O ( V ) O(V) O(V)

典型例题

OpenJudge2755

有一个容积为 40 的背包,给定 n n n 个物品,体积分别为 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1a2...an,问从这些物品中选择一些恰巧装满背包,共用多少种不同方案?

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using namespace std;

int n,a[30],dp[50][50];

int main()
{
	scanf("%d",&n);
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		dp[i][0]=1;
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=40;j++){
			dp[i][j]=dp[i-1][j];
			if(j-a[i]>=0){
				dp[i][j]+=dp[i-1][j-a[i]];
			}
		}
	}
	printf("%d\n",dp[n][40]);
	return 0;
}

POJ 3624

#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
using namespace std;
const int maxn=3402;
const int maxm=13000;
int n,m,dp[maxm];
struct bag{
	int w,d;
}a[maxn];

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&a[i].w,&a[i].d);
//	for(int i=0;i<a[1].w;i++)
//		dp[i]=0;
//	for(int i=a[1].w;i<=m;i++)
//		dp[i]=a[1].d;
	memset(dp,0,sizeof(dp));
//	for(int i=2;i<=n;i++)
	for(int i=1;i<=n;i++)
		for(int j=m;j>=a[i].w;j--)
			dp[j]=max(dp[j],dp[j-a[i].w]+a[i].d);
	printf("%d\n",dp[m]);
	return 0;
}

完全背包

  • 有 N 种物品和一个容量为 V 的背包。 第 i 件物品体积是 Wi,价值是Vi。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大。
  • 特点:每种物品有无数件,可以选择 0 或多件。
  • f ( i , j ) = m a x { f ( i − 1 , j ) , f ( i − 1 , j − k × w i ) + k × v i ∣ k = 0 , . . . , V / w i } f(i,j)=max\{f(i-1,j),f(i-1,j-k×w_i)+k×v_i|k=0,...,V/w_i\} f(i,j)=max{f(i1,j),f(i1,jk×wi)+k×vik=0,...,V/wi}
memset(f,0,sizeof(f));
for(int i=1;i<=N;i++)
	for(int j=w[i];j<=V;j--)
		f[j]=max(f[j],f[j-w[i]]+v[i]);
ans=f[V];
  • 0-1背包中,采用逆序循环是为了保证物品只被选取一次,即决策选入第 i i i 件物品时,依据的时一个绝无已经选入第 i i i 件物品的子结果 f [ i − 1 ] [ j − w [ i ] ] f[i-1][j-w[i]] f[i1][jw[i]]

  • 现在完全背包考虑加选一个物品时,需要一个可能已经选入第 i i i 中物品的子结果,故选用正序循环。

  • 复杂度

  • 时间复杂度 —— O ( N × V ) O(N×V) O(N×V)

  • 空间复杂度 —— O ( V ) O(V) O(V)

多重背包问题

  • 有 N 件物品和一个容量为 V 的背包。 第 i 件物品体积是 Wi,价值是Vi,有 Ci 件可用,求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且总价值最大

  • 特点:每种物品为有限件

  • f ( i , j ) = m a x { f ( i − 1 , j ) , f ( i − 1 , j − k × w i ) + k × v i ∣ k = 0 , . . . , C i } f(i,j)=max\{f(i-1,j),f(i-1,j-k×w_i)+k×v_i|k=0,...,C_i\} f(i,j)=max{f(i1,j),f(i1,jk×wi)+k×vik=0,...,Ci}

  • 时间复杂度为 O ( V × ∑ n = 1 N C i ) O(V×\sum_{n=1}^N C_i) O(V×n=1NCi)

  • 二进制拆分优化

    • 采用进制的思想将 C i C_i Ci 进行二进制拆分,然后转换为0-1背包问题。
    • 优化目标:让组数尽可能少,又能够覆盖所有决策
int cnt=0;
for(int i=1;i<=N;i++){
	int t=C[i];
	for(int k=1;k<=t;k<<=1){
		cnt++;
		vv[cnt]=k*v[i];
		ww[cnt]=k*w[i];
		t-=k;
	}
	if(t>0){
		cnt++;
		vv[cnt]=t*v[i];
		ww[cnt]=t*w[i];
	}
}
  • 处理后直接对数组 vv 和 ww 进行 0-1背包,注意 N 变为 cnt
  • 时间复杂度优化为 O ( N × V × ∑ n = 1 N l o g C i ) O(N×V×\sum_{n=1}^N logC_i) O(N×V×n=1NlogCi)
  • 另附一道例题:题目链接
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

int Cash,N,cnt,c[20],w[20],ww[10010],dp[100010];

int main()
{
	while(~scanf("%d%d",&Cash,&N)){
		cnt=0;
		for(int i=1;i<=N;i++){
			scanf("%d%d",&c[i],&w[i]);
			for(int j=1;j<=c[i];j<<=1){
				ww[++cnt]=j*w[i];
				c[i]-=j;
			}
			if(c[i]>0){
				ww[++cnt]=c[i]*w[i];
			}
		}	
		memset(dp,0,sizeof(dp));
		for(int i=1;i<=cnt;i++){
			for(int j=Cash;j>=ww[i];j--){
				dp[j]=max(dp[j],dp[j-ww[i]]+ww[i]);
			}
		}
		printf("%d\n",dp[Cash]);
	}
	return 0;
}

分组背包问题

  • 有 N 件物品和一个容量为 V 的背包,第 i 种物品的体积是 Wi ,价值 Vi,将所有的物品划分成若干组,每个组里面的物品最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大 。
  • 特点是:每种物品有 1 件,每组只能选 1 件
  • 状态转移方程:
  • 对于每个组:不取、取1号、…、取 s s s
  • 定义 f ( k , j ) f(k,j) f(k,j) 表示前 k k k 组中,容量不超过 j j j 的最大价值
  • f ( k , j ) = m a x { f ( k − 1 , j ) , f ( k − 1 , j − w u ) + v u ∣ u = 1 , . . . , s } f(k,j)=max\{f(k-1,j),f(k-1,j-w_u)+v_u|u=1,...,s\} f(k,j)=max{f(k1,j),f(k1,jwu)+vuu=1,...,s}
//S表示组数,s[i]表示每组的物品数
for(int k=1;k<=S;k++){
	for(int j=0;j<=V;j++){
		for(int u=1;u<=s[k];u++){
			f[k][j]=f[k-1][j];
			if(j-w[k][u]>=0)
				f[k][j]=max(f[k][j],f[k-1][j-w[k][u]]+v[k][u]);	
		}
	}
} 
//滚动数组优化
for(int k=1;k<=S;k++)
	for(int j=V;j>=w[k][u];j--)
		for(int u=1;u<=s[k];u++)
			f[j]=max(f[j],f[j-w[k][u]]+v[k][u]);

HDU 1712

N N N 门课,有 M M M 天时间复习, a [ i ] [ j ] a[i][j] a[i][j] 表示第 i i i 门课复习 j j j 天所能获得的期望分数,问所能获得的最大分数为多少?

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn=110;
int a[maxn][maxn],dp[maxn];

int main()
{
	int N,M;
	while(~scanf("%d%d",&N,&M)){
		if(N==0&&M==0)
			break;
		for(int i=1;i<=N;i++){
			for(int j=1;j<=M;j++){
				scanf("%d",&a[i][j]);
			}
		}
		memset(dp,0,sizeof(dp));
		for(int i=1;i<=N;i++)
			for(int j=M;j>=0;j--)
				for(int k=1;k<=j;k++)
						dp[j]=max(dp[j],dp[j-k]+a[i][k]);
		printf("%d\n",dp[M]);
	}
	return 0;
}

超大背包问题

  • 有 N 件物品和一个容量为 V 的背包。第 i 种物品的体积是 Wi ,价值 Vi。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
  • 约束:N<=500, M, Wi<=1e9, 1<=v[1]+…+v[n]<=5000
  • 思路:因为背包的容量<=1e9,会超内存,但价值的总和并不大,所以修改状态条件,定义 dp[i][j] = 选取前 i 个物品背包价值为 j 时的最小重量即可。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;

int t,n,B,w[510],v[510],dp[5010];

int main()
{
	scanf("%d",&t);
	while(t--){
		scanf("%d%d",&n,&B);
		int sum=0;
		for(int i=1;i<=n;i++){
			scanf("%d%d",&w[i],&v[i]);
			sum+=v[i];
		}
		memset(dp,0x3f,sizeof(dp));
		dp[0]=0;
		for(int i=1;i<=n;i++){
			for(int j=sum;j>=v[i];j--){
				dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
			}
		}
		for(int i=sum;i>=0;i--){
			if(dp[i]<=B){
				printf("%d\n",i);
				break;
			} 
		}
	}
	return 0;
}
  • 若修改约束为:N <= 40, Wi <= 1e15 , Vi <= 1e15
  • 特点是:0-1 背包问题变种,体积巨大。与 DP 无关!
  • 继续依照 0 - 1背包的思路求解的话,时间复杂度和空间复杂度都无法承受
  • 思路:由于 N 很小但 V 很大,故可采用折半枚举的方法
    • 首先将物品分成两组,每组 N/2 个物品
    • 对这两组物品分别进行子集枚举,得到一系列的 <w1,v1> 二元组,代表该集合的总体积、总价值
    • 考虑单个组:
      • 如果 a[i].w1 > a[j].w1 且 a[i].v1 < a[j].v1,那么 a[i] 就可以舍去
      • 剩下的所有二元组,一定满足 a[i].w1 < a[j].w1 且 a[i].v1 < a[j].v1 ,这样就可以排序了
    • 对于排好序的两个组,假设分别为组1 和组 2,枚举组 1 的所有二元组,再于组 2 中进行二分查找,找体积小于
      V-w1 所对应的 v1 的最大值,就可以找到最大值
  • 复杂度分析
    • 分组+枚举: 2 × 2 N / 2 × ( N / 2 ) = N × 2 N / 2 2 × 2^{N/2} × (N/2) = N × 2^{N/2} 2×2N/2×(N/2)=N×2N/2
    • 后续枚举+二分: 2 N / 2 × l o g ( 2 N / 2 ) = ( N / 2 ) × 2 N / 2 2^{N/2} × log(2^{N/2}) = (N/2) × 2^{N/2} 2N/2×log(2N/2)=(N/2)×2N/2
    • 因此时间复杂度为 O ( N × 2 N / 2 ) O(N × 2^{N/2}) O(N×2N/2)
    • 代码实现(《挑战程序设计竞赛》 P163)
#include <cstdio>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
typedef long long ll;
const int maxn = 40 + 5;
const int INF = 10000000;
 
int n;
ll w[maxn], v[maxn];
ll W;
 
pair<ll, ll> ps[1 << (maxn / 2)];    //(重量, 价值)
void solve()
{
    //枚举前半部分
    int n2 = n / 2;
    for (int i = 0; i < 1 << n2; i++){
        ll sw = 0, sv = 0;
        for (int j = 0; j < n2; j++){
            if (i >> j & 1){
                sw += w[i];
                sv += v[i];
            }
        }
        ps[i] = make_pair(sw, sv);
    }
 
    //去除多余的元素
    sort(ps, ps + (1 << n2));
    int m = 1;
    for (int i = 1; i < 1 << n2; i++){
        if (ps[m - 1].second < ps[i].second){
            ps[m++] = ps[i];
        }
    }
 
    //枚举后半部分并求解
    ll res = 0;
    for (int i = 0; i < 1 << (n - n2); i++){
        ll sw = 0, sv = 0;
        for (int j = 0; j < n - n2; j++){
            if (i >> j & 1){
                sw += w[n2 + j];
                sv += v[n2 + j];
            }
        }
        if (sw <= W){
            ll tv = (lower_bound(ps, ps + m, make_pair(W - sw, INF)) - 1) -> second;
            res = max(res, sv + tv);
        }
    }
    printf("%lld\n", res);
}

CodeForces 1132E

八组数 ( 1 , 2 , . . . , 8 ) (1,2,...,8) (1,2,...,8),每组数有 c n t i ( 0 ≤ c n t i ≤ 1 0 16 ) cnt_i(0≤cnt_i≤10^{16}) cnti(0cnti1016) 个,背包的容量为 W ( 0 ≤ W ≤ 1 0 18 ) W(0≤W≤10^{18}) W(0W1018),问最多能装多少?

  • 同样这道题的数据量太大了,不可能直接套0-1背包的板子求解,需要进行一步转换
  • 一道思维DP,自己没想出来,参考了别人的题解,这题处理方法确实有点巧妙…
  • 1 1 1 8 8 8 的最小公倍数为 840 840 840,类似于贪心,按照 840 840 840 对背包进行切分,对于每一个数先优先凑成 840 840 840 的物品放进背包,剩下物品体积必然小于 840 ∗ 8 840*8 8408,那么答案就转化为 840 ∗ x + y 840 * x + y 840x+y 的形式其中 y < 840 ∗ 8 y < 840 * 8 y<8408,这样就只用考虑每种物品凑不够 840 840 840的情况。
  • 解法一: d p [ i ] [ j ] dp[i][j] dp[i][j]表示已经考虑完前 i i i 种物品后能否凑成重量 j j j
//https://blog.csdn.net/CSDN_PatrickStar/article/details/89761102?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-1&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-1
#include <bits\stdc++.h>
using namespace std;
typedef long long ll;
const int N = 8400;
ll cnt[9], pre[9];
ll dp[9][N];

int main() {
    ll W, sum = 0, w = 0, ans = 0;
    cin >> W;
    for(int i = 1 ; i <= 8 ; i++){
        cin >> cnt[i];
        sum += cnt[i]*i;
        pre[i] = min(1LL*840/i, cnt[i]);
        cnt[i] -= pre[i];
        w += min(cnt[i], (max(0LL, W-840)-w)/i)*i;
    }
    if(sum <= W){
        return cout << sum , 0;
    }
    dp[0][0] = 1;
    for(int i = 1 ; i <= 8 ; i++){
        for(int j = 0 ; j <= 840*8 ; j++){
            for(int k = 0 ; k <= min(pre[i], 1LL*j/i) ; k++){
                dp[i][j] = dp[i][j]|dp[i-1][j-k*i];
            }
        }
    }
    for(int j = 0 ; j <= W-w ; j++){
        if(dp[8][j]){
            ans = w+j;
        }
    }
    cout << ans << endl;
    return 0; 
}

  • 解法二:这个解法可能更好理解一些, d p [ i ] [ j ] dp[i][j] dp[i][j]表示已经考虑完前 i i i 种物品后取出 j j j 重量的物品时能获得 840 840 840 的最大份数,不用来凑 840 840 840 的部分是给 j j j 的,其他的都用来凑 840 840 840
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long ll;

ll W,dp[10][8*840+10],cnt[10];

int main()
{
	scanf("%lld",&W);
	for(int i=1;i<=8;i++){
		scanf("%lld",&cnt[i]);
	}
	memset(dp,-1,sizeof(dp));
	dp[0][0]=0;
	for(int i=1;i<=8;i++){
		for(int j=0;j<=8*840;j++){
			int min_k=min(cnt[i],(ll)840/i);
			for(int k=0;k<=min(min_k,j/i);k++){
				if(dp[i-1][j-k*i]!=-1)
					dp[i][j]=max(dp[i][j],dp[i-1][j-k*i]+(cnt[i]-k)/(840/i));
			}
		}
	}
	ll ans=0;
	for(ll i=0;i<=8*840;i++){
		if(i<=W&&~dp[8][i])
			ans=max(ans,i+840*min(dp[8][i],(W-i)/840));	
		if(i>W)
			break;
	}
	printf("%lld\n",ans);
	return 0;
}

背包问题输出路径

题目链接

memset(dp,0,sizeof(dp));
for(int i=1;i<=M;i++){
	for(int j=0;j<=N;j++){
		dp[i][j]=dp[i-1][j];
		if(j-w[i]>=0){
			dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+w[i]);
		}
	}		
}
int tmp=N;
for(int i=M;i>1;i--){
	if(dp[i][tmp]==dp[i-1][tmp])
		path[i]=0;
	else{
		path[i]=1;
		tmp-=w[i];
	}
}
path[1]=(dp[1][tmp]>0)?1:0;
for(int i=1;i<=M;i++){
	if(path[i])
		printf("%d ",w[i]);
}
memset(dp,0,sizeof(dp));
memset(path,0,sizeof(path));
for(int i=1;i<=M;i++){
	for(int j=N;j>=w[i];j--){
		if(dp[j]<dp[j-w[i]]+w[i]){
			dp[j]=dp[j-w[i]]+w[i];
			path[i][j]=1;
		}
	}
}
vector<int> p;
for(int i=M,m=N;i>=1&&m>=0;i--){
	if(path[i][m]){
		p.push_back(w[i]);
		m-=w[i];
	}
}
for(int i=p.size()-1;i>=0;i--)
	printf("%d ",p[i]);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值