【挑战程序设计竞赛】- 2.3动态规划(01背包+LCS+完全背包+多重部分和+LIS+划分数+多重划分数)

2.3动态规划


DP dynamic programming

2.3.1记忆化搜索(01背包+LCS)

例1 01背包问题:

有n个重量和价值分别为 w i w_i wi v i v_i vi的物品。选出总重量不超过W的物品。求挑选方案的价值总和最大值。
1<=n<=100, 1<= w i , v i w_i,v_i wi,vi<=100, 1<=W<=10000

思路:
(1) 首先是朴素想法:都来试试看每个物品是否放入背包。

int n, W;
int w[NMAX], v[NMAX];
//从第i个物品开始挑选,总重小于totw
int rec(int i, int totw){
    int res;
    if( i==n ){
        res = 0;
    }else if( totw < w[i] ){ //不能挑选
        res = rec( i+1, totw );
    }else{ //可能挑选或者不挑选
        res = max( rec( i+1, totw ), rec( i+1, totw-w[i] ) + v[i] );
    }
    return res;
}

void solve(){
    printf("%d\n", rec(0,W) );
}

(2) 记忆化搜索:记录第一次搜过的位置,存在数组里

int n, W;
int w[NMAX], v[NMAX];
int dp[NMAX][WMAX];
//从第i个物品开始挑选,总重小于totw
int rec(int i, int totw){
//-----change:
    if(dp[i][totw]>=0){
        return dp[i][totw];
    }
    int res;
    if( i==n ){
        res = 0;
    } else if( totw < w[i] ){ //不能挑选
        res = rec( i+1, totw );
    } else{ //可能挑选或者不挑选
        res = max( rec( i+1, totw ), rec( i+1, totw-w[i] ) + v[i] );
    }
//-----change:
    return dp[i][j] = res;
}

void solve(){
//-----change:
    memset(dp, -1, sizeof(dp)); //初始化
    printf("%d\n", rec(0,W) );
}

(3) 再分析记忆化数组:

一、dp[i] [j] 代表从第i个物品开始挑选,总重小于totw的最大价值和。

dp[n] [w] = 0
dp[i] [j] = dp[ i + 1 ] [ j ] (when w[ i ] > j )
max( dp[ i + 1 ] [ j ], dp[ i+1 ] [ j - w[ i ] ] + v[ i ] ) (else)

写下来就是:

int dp[NMAX][WMAX];
void solve(){
    memset(dp, 0, sizeof(dp));
    for(int i = n-1; i>=0; i--){ //看i是根据后面大的值得来
        for(int j = 0; j<=W; j++){ //看j是根据小的得来
            if(j<w[i]){
                dp[i][j] = dp[i+1][j];
            }
            else{
                dp[i][j] = max(dp[i+1][j], dp[i+1][j-w[i]]+v[i]);
            }
        }
    }
    printf("%d\n", dp[0][W]);
}

二、另一个角度,如果dp[i] [j]代表0~i-1个物品里,总重小于j的最大价值和。

dp[0] [j] = 0;
dp[i+1] [j] = dp[i] [j] , j < w[i]
max( dp[ i ] [ j ] , dp[i] [ j-w[i] ] + v[i] ) , else

写下来是正向的循环:

for(int i=0; i<n; i++){
    for(int j=0; j<=W; j++){
        if(j<w[i]){
        	dp[i+1][j] = dp[i][j];
        }
        else{
            dp[i+1][j] = max(dp[i][j], dp[i][j-w[i]]+v[i]);
        }
    }
}
printf("%d\n", dp[n][W]); //最后答案也不同

三、另另一个角度,dp[i] [j]还是代表0~i-1个物品里,总重不超过j的最大价值和。

那么与当前有关的状态满足:
dp[ i + 1 ] [ j ] = max( dp[ i+1 ] [ j ] , dp[ i ] [ j ] );
dp[ i + 1 ] [ j - w[ i ] ] = max( dp[ i + 1 ] [ j - w[ i ] ] , dp[ i ] [ j ] + v[ i ] ) (only when j>=w[i])

for(int i=0; i<n; i++){
    for(int j=0; j<=W; j++){
        dp[ i+1 ][ j ] = max( dp[ i+1 ][ j ] , dp[ i ][ j ] );
        if(j<w[i]){
        	dp[i+1][j-w[i]] = max( dp[i+1][j-w[i]] , dp[i][j]+v[i] );
        }
    }
}
printf("%d\n", dp[n][W]); //最后答案也不同

例2 最长公共子序列:

给定长度为n和m的字符串s和t,求最长公共子序列的长度。
1<=n,m<=1000

dp[i] [j] 代表到s[0~i -1 ] 和 t[0~j-1 ]间最长公共子序列长度。

dp[i+1] [j+1] = max( dp[ i ] [j+1] , dp[i+1] [j] );
dp[i+1] [j+1] = max( dp[ i] [j]+1 , dp[i+1] [j+1] ); (only when s[i] = t[j] )

int n,m;
char s[NMAX],t[MMAX];
void solve(){
	for(int i=0; i<n; i++){
        for(int j=0; j<m; j++){
            dp[i+1][j+1] = max(dp[i][j+1], dp[i+1][j]);
            if(s[i] == t[j]){
                dp[i+1][j+1] = max(dp[i+1][j+1], dp[i][j]+1);
            }
        }
    }
    printf("%d\n",dp[n][m]);
}

2.3.2 递推关系(完全背包/01背包加强/多重部分和/LIS)

例题1 完全背包:
有n重量和价值分别为 w i w_i wi v i v_i vi的物品。每种物品可选若干。
选出总重量不超过W的物品。求挑选方案的价值总和最大值。
1<=n<=100, 1<= w i , v i w_i,v_i wi,vi<=100, 1<=W<=10000

思路:dp[i] [j] 代表0~i-1种物品,重量不超过j的最大价值和。
dp[i+1] [j] = max(dp[i] [j],
dp[i] [j-w[i] ] + v[i],
dp[i] [j-2 * w[i] ] + 2v[i],
dp[i] [j-3 * w[i] ] + 3
v[i],…)

那么自然的会整出三重循环:

for(int i=0; i<n; i++){
    for(int j=0; j<=W; j++)
    	for(int k=0; k*w[i]<=j; k++){
        	dp[i+1][j] = max( dp[i+1][j], dp[i+1][j-k*w[i]]+k*v[i]);
        }
    }
}

观察发现有些重复的计算,k>=1时
dp[i+1] [j]中选 k个i种 物品 = 在dp[i+1] [j-w[i]] 选 k-1个i种 物品

dp[i+1] [j] 中的递推部分 其实已经在dp[i+1] [j-w[i]]中完成,因此:

dp[i+1] [j] = max(dp[i] [j], dp[i+1] [j-w[i]]+v[i] )
(该式子与01背包的不同在于这里是i+1而不是i)

for(int i=0; i<n; i++){
    for(int j=0; j<=W; j++)
        dp[i+1][j] = dp[i][j];
    	if(j>=w[i]){
        	dp[i+1][j] = max( dp[i+1][j], dp[i+1][j-w[i]]+v[i]);
        }
    }
}

另外这里看到其实可以把二维数组降成一维(去掉i)。
但是容易有bug,i+1和i之间微妙的循环方向区别。要想清楚哦。

例2 01背包(范围有修改)

有n个重量和价值分别为 w i w_i wi v i v_i vi的物品。选出总重量不超过W的物品。求挑选方案的价值总和最大值。
1<=n<=100, 1<= v i v_i vi<=100, 1<= w i w_i wi<=1e7, 1<=W<=1e9

这里dp[i+1] [j] 如果还是前i个重量不大于j的价值最大值就没了。开不了那么大数组
改变dp含义:dp[i+1] [j]表示前i个价值为j的最小重量,不存在时为INF

dp[0] [0] = 0;
dp[0] [j] = INF;
dp[i+1] [j] = min( dp[i] [j-v[i]]+w[i], dp[i] [j] );

dp[i] [j]<=W时的最大j

int dp[NMAX][NMAX*VMAX];
void solve(){
    fill(dp[0],dp[0]+NMAX*VMAX+1, INF);
    dp[0][0] = 0;
    for(int i=0; i<n; i++){
        for(int j=0; j<=NMAX*VMAX; j++){
            dp[i+1][j] = dp[i][j];
            if(j>=v[i]){
                dp[i+1][j] = min( dp[i+1][j], dp[i][j-v[i]]+w[i] );
            }
        }
    }
    int res = 0;
    for(int i=0;i<=NMAX*VMAX;i++) 
        if(dp[n][i]<=W) res = i;
    printf("%d\n",res);
}

例3 多重部分和

有n种不同大小的数字 a i a_i ai,每种各 m i m_i mi个,问是否可以选择若干的数字使和恰好为K。
1<=n<=100, 1<= a i , m i a_i,m_i ai,mi<=1e5, 1<=K<=1e5

思路:

dp[i+1] [j] 表示:前i种数字是否能加和为j 值为true或false
dp[i+1] [j] = dp[i] [j-k*a[i]], when 0<=k<=m[i] 且 j>=k * a[i]
——这种想法要三重循环。并且没法化简
通常dp数组不用来表示bool。

另一种想法:

dp[i+1] [j]表示前i种数字加和得到 j 时,第i种数字最多还剩几个。

不能得到默认-1。

前i-1种数字加和得到j,那么:dp[i+1] [j] = m[i];

前i种数字加和得到 j-a[i] 时第i种数字还剩k个,那么:
前i种数字加和得到 j-a[i]*2 时第i种数字还剩k-1个…:

dp[i+1] [j] = dp[i+1] [j-a[i]]-1 (when dp[i+1] [j-a[i]] >0)

int dp[NMAX][KMAX];
int m[NMAX], a[NMAX];
void solve(){
    memset(dp,-1,sizeof(dp));
    for(int i=0; i<n; i++){
        dp[i][0]=m[i];
        for(int j=1;j<KMAX; j++){
            if(dp[i][j]>=0){
                dp[i+1][j]=m[i];
            }
            else if(j>=a[i] && dp[i+1][j-a[i]]>0){
                dp[i+1][j]= dp[i+1][j-a[i]]-1;
            }
        }
    }
    if(dp[n][K]>=0){
        printf("YES\n");
    }else{
        printf("NO\n");
    }
}

例4. 最长上升子序列(LIS)

对于长度为n的数组a,求最长上升子序列长度:对于i<j, 满足 a i < a j a_i<a_j ai<aj

思路:

一、定义dp[i] = 以a[i]结尾的最长上升子序列长度

dp[i] = max(1, dp[j]+1 when a[j]<a[i] 且j<i)

复杂度 O ( n 2 ) O(n^2) O(n2)

int n;
int a[NMAX];
int dp[NMAX];
void solve(){
    int res = 0;
    for(int i=0;i<n;i++){
        dp[i]=1;
        for(int j=0; j<i; j++){
            if(a[j]<a[i]){
                dp[i] = max(dp[i],dp[j]+1);
            }
        }
        res = max(res,dp[i]);
    }
    printf("%d\n",res);
}

二、定义dp[i] = 最长上升子序列中第 i 位最小的数字

lower_bound(a,a+n,k) 已排序好的数组a中满足a>=k的指针位置

int n;
int a[NMAX];
int dp[NMAX];
void solve(){
   	fill(dp,dp+n,INF);
    int cnt = 1;
    for(int i=0; i<n; i++){
        *lowerbound(dp,dp+cnt+1, a[i])=a[i];
    }
    printf("%d\n",lowerbound(dp,dp+cnt+1, INF)-dp);
}

2.3.2 计数DP(划分数, 多重划分数)

例题1 划分数:

有n个无区别物品,将它们划分成不超过m组,求划分方法数模M的余数。

1<=m<=n<=1000, 2<=M<=10000

思路:

dp[i] [j] = j的i划分数(将j分成不超过i组的划分方法数)

如果简单考虑 dp[i] [j] = sum( dp[i-1] [j-k] ) (0<=k<=j),则会出错,重复计算1+1+2和1+2+1。X

正确想法:

我们现在有sum( a[i] ) = dp[i] [j],分为两种情况:
如果所有的a[i]都大于0(恰好分成i份),那么每个-1,正好是j-i的i划分;
如果其中有a[i]为0,那么正好是j的i-1划分。

dp[i] [j] = dp[i] [j-i] + dp[i-1] [j];

int n,m;
int dp[MMAX][NMAX];
void solve(){
    dp[0][0] = 1;
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            dp[i][j] = dp[i-1][j];
            if(j>=i){
            	dp[i][j] = (dp[i-1][j]+dp[i][j-i])%M;
            }
        }
    }
    printf("%d\n",dp[m][n]);
}

例题2 多重集组和数

有n种物品,第i种ai个。不同种类可以区分,相同种类不可区分。取出m个物品,有多少种取法。(模M)
1<=n,m, a i a_i ai<=1000, 2<=M<=10000

想法:dp[i+1] [j] = 前i种取出j个物品的取法。

dp[i+1] [j] = sum( dp[i] [j-k] ) , 0<=k<=min( a[i] ,j )

复杂度O(nm2)

化简后:

if( j<=a[i] ) dp[i+1] [j] = dp[i] [0] + dp[i] [1] +… + dp[i] [j-1] + dp[i] [j]
dp[i+1] [j] = dp[i] [j] +dp[i+1] [j-1]

if( j>=a[i]+1 ) dp[i+1] [j] = dp[i] [j-1] + dp[i] [j-2] +… +dp[i] [ j-a[i] ]
dp[i+1] [j] = dp[i] [j-1] + dp[i] [j-2] +… +dp[i] [ j-a[i] ] + dp[i] [ j-a[i]-1 ] + …+ dp[i] [0]
-( dp[i] [ j-a[i] -1] + …+ dp[i] [0] )
= dp[i] [j] + dp[i+1] [j-1] - dp[i] [j-a[i]-1]

复杂度O(nm)

int n,m;
int a[NMAX];
int dp[NMAX][MMAX];
void solve(){
for(int i=0; i<n; i++){
    dp[i][0]=1;
}
for(int i=0; i<n; i++){
    for(int j=1; j<=m;j++){
        if(j<=a[i]){
            dp[i+1] [j] = dp[i] [j] +dp[i+1] [j-1];
        }else{
            dp[i+1] [j] = dp[i] [j] + dp[i+1] [j-1] - dp[i] [j-a[i]-1];
        }
    }
}
    printf("%d\n",dp[n][m]);
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值