个人整理的算法笔记:参考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(i−1,j),f(i−1,j−wi)+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 a1,a2,...,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(i−1,j),f(i−1,j−k×wi)+k×vi∣k=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[i−1][j−w[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(i−1,j),f(i−1,j−k×wi)+k×vi∣k=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(k−1,j),f(k−1,j−wu)+vu∣u=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(0≤cnti≤1016) 个,背包的容量为 W ( 0 ≤ W ≤ 1 0 18 ) W(0≤W≤10^{18}) W(0≤W≤1018),问最多能装多少?
- 同样这道题的数据量太大了,不可能直接套0-1背包的板子求解,需要进行一步转换
- 一道思维DP,自己没想出来,参考了别人的题解,这题处理方法确实有点巧妙…
- 1 1 1 到 8 8 8 的最小公倍数为 840 840 840,类似于贪心,按照 840 840 840 对背包进行切分,对于每一个数先优先凑成 840 840 840 的物品放进背包,剩下物品体积必然小于 840 ∗ 8 840*8 840∗8,那么答案就转化为 840 ∗ x + y 840 * x + y 840∗x+y 的形式其中 y < 840 ∗ 8 y < 840 * 8 y<840∗8,这样就只用考虑每种物品凑不够 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]);