浅谈动态规划(三)

来源:互联网 发布:广数980g76编程实例 编辑:程序博客网 时间:2024/06/05 19:02

背包问题

一、01背包

问题描述:
给定n种物品和一个背包。物品i的价值是Wi,其体积为Vi,背包的容量为C。可以选择任意装入背包中的物品,求装入背包中物品的最大总价值。

对于一种物品,要么装入背包,要么不装。所以对于一种物品的装入状态可以取0和1。我们设物品i的装入状态为xi,xi∈ (0,1),此问题称为0-1背包问题。

设dp[i][j]表示把前i个物品装入容量为j的背包的最大总价值,则状态转移方程可以表示为:

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

另外,还可以优化空间,降为一维(用到滚动数组),只不过需要逆序:

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

二、完全背包

问题描述:
给定n种物品和一个背包。第i种物品的价值是Wi,其体积为Vi,背包容量为C,同一种物品的数量无限多。可以选择任意装入背包中的物品,求装入背包中物品的最大总价值。

转化为01背包问题求解:

考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品

更高效的转化方法是:

把第 i 种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<=V。

这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。

这样把每种物品拆成O(log(V/c[i]))件物品,是一个很大的改进。

但我们有更优的O(VN)的算法:

for(int i=0;i<n;i++)    for(int j=0;j<=V;j++)    //将01背包降一维的逆序改为顺序即可        d[j]=max(d[j],d[j-v[i]]+w[i]);

(想想为什么可以这样做?试着从贪心的角度去分析)

三、多重背包

问题描述:
给定n中物品和一个背包。第i种物品的价值是Wi,其体积为Vi,数量是Ki件,背包的容量为C,可以任意选择装入背包中的物品,求装入背包中物品的最大总价值。

基本方法是转化为01背包求解:

把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解.

 for(int i=0;i<n;i++)    for(int j=1;j<=k[i];j++)      //多重背包,最简单的做法就是插入一个for循环        for(int p=C;p>=v[i];p--)      //多重背包变01背包,降一维要逆序                          dp[p]=max(dp[p],dp[p-v[i]]+w[i]);

还可以优化吗?

优化思路:
因为考虑到我们需要凑出0~num[i]个数目,可以证明,用二进制的拆分可以是件数达到最小。

二进制方法

将第 i 种物品分成若干件物品,每件物品有一个系数,这件物品的体积和价值均是原来的体积和价值乘以这个系数,使这些系数分别为

  1,2,4,...,2^(k-1),num[i]-2^k+1

k是满足num[i]-2^k+1的最大整数。

例如num[i]=13,则将这种物品分成系数分别是1,2,4,6的四件物品。

下面给出部分代码

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

四、混合背包

问题描述:

一个旅行者有一个最多能用V公斤的背包,现在有n件物品,它们的重量分别是W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

数据说明:
第一行:V(V<=200),N(N<=30);
第2..N+1行:Wi,Ci,Pi,前两个整数分别表示每个物品的重量,价值,第三个整数若为0,则说明此物品可以购买无数件,若为其他数字,则为此物品可购买的最多件数(Pi)。

把每一样物品分成三种情况讨论即可:

for(int i=0;i<n;i++){    if(num[i]==0)        for(int  j=0;j<V;j++)        //完全背包            d[j]=max(d[j],d[j-v[i]]+w[i]);    else  if(num[i]==1)        for(int j=V;j>=0;j--)       //01背包            d[j]=max(d[j],d[j-v[i]]+w[i]);    else        多重背包(略);}

数位dp

第一种解决办法

问题描述:
不吉利的数字为所有含有4或62的号码。你的要求是在任意区间内,找出不含不吉利数字的个数。

d[i][j]表示开头是j的i位数满足条件的有多少个,则状态转移方程为:

  d[i][j] += d[i-1][k]  k=0,1,2,3,4,...,9     (k != 2 || j != 6)

则有如下预处理:

    for(int i=1;i<=7;i++)         for(int j=0;j<10;j++)          //枚举第i位可能出现的数            for(int k=0;k<10;k++)       //枚举第i-1位可能出现的数                if(j!=4&&!(j==6&&k==2))                    dp[i][j]  += dp[i-1][k];  

接下来,需要对把n转换成字串的p[len]进行操作:

for(int i=len-1;i>=1;i--){   for(int j=0;j<d[i];j++)      if(d[i+1]!=6||j!=2)         ans+=dp[i][j];   if(d[i]==4||d[i+1]==6&&d[i]==2)    /*当前数字为‘4’或者当前位与上一位构         break;                         成‘62’ ,则后面就不必计数了*/ }

最后ans的值即所求!

另一种方法:

问题描述:
给一个数字n,范围在1~2^63-1,求1~n之间含有49的数字有多少个。

同样先构造一个二维dp[i][j]状态,表示如下:

dp[i][0]    代表长度为 i 并且不含有49的数字的个数;dp[i][1]    代表长度为 i 并且不含有49,但是最高位是9的数字的个数;dp[i][2]    代表长度为 i 并且含有49的数字的个数。

d[i][j]的一维赋三种状态,那么就必须把三者联系起来:

dp[i][0] = dp[i-1][0] * a[i] - dp[i-1][1]

表示长度为 i 的不含有49的数字的个数等于长度为 i - 1 的不含有49的数字的个数*当前的数字,因为这个位置可以填0~a[i] - 1,然后再减去长度为 i - 1 的最高位是9的数字的个数,因为如果长度为 i - 1 的最高位是9的话,那么高一位就不能填4了,否则就组成了49。

dp[i][1] = dp[i-1][0]

表示长度为 i 的并且不含有49同时最高位是9的数字的个数等于,长度为 i - 1 的不含有49的数字的个数,因为只要在它的高一位加上一个9就可以了。

dp[i][2] = dp[i-1][2] * a[i] + dp[i-1][1]

表示长度为 i 的含有49的数字的个数等于,长度为 i - 1 的数字的个数*当前的数字,再加上长度为 i - 1 的并且不含有49同时最高位是9的数字的个数,因为这个时候,只要在高一位加上一个4就可以了,这样在最高的两位就组成了一个49。

然后让数组 a[i] 从低位到高位存储 n 的每一位数字。并进行如下操作:

(1)首先加上长度为 i - 1 的符合条件的数字个数;(2)再讨论以前是不是出现过49,如果出现过,就要再追加上长度为 i - 1 的不符合条件的数字的个数,因为以前已经有49了;(3)如果没有出现过,就要判断这一位是不是大于4呢,如果大于4,就要再追加上长度为 i - 1 的不含有49但是最高位是9的数字的个数,因为这个时候可以再这一位填4;(4)然后就是判断一下,当前位和上一位是不是满足49,如果满足,标记出现了49了!为以后的判断做准备。

代码分为两步处理:

(1)预处理:

    memset(dp,0,sizeof(dp));    dp[0][0]=1;    for(int i=1;i<22;i++)    {        dp[i][0]=dp[i-1][0]*10-dp[i-1][1];        dp[i][1]=dp[i-1][0];        dp[i][2]=dp[i-1][2]*10+dp[i-1][1];    }

(2)计数操作:

    for(i=len-1;i>=1;i--)    {        ans+=dp[i-1][2]*d[i];        if(flag)          ans+=dp[i-1][0]*d[i];        if(!flag&&d[i]>4)          ans+=dp[i-1][1];        if(d[i+1]==4&&d[i]==9)                   flag=1;    } 

注意:调用的时候是(n+1),譬如49,如果只是调用49,则不属于40的范畴之内,因此这一个无法计算。必须要填上50才行。

背包题目:

UVa 10154

UVa 562

UVa 357

POJ 1948

POJ 1745

POJ 1014

ZOJ 3812

Codeforces 106C

hdu 5000

hdu 5410

hdu 1059

hdu 3466

hdu 2639

hdu 3496

hdu 3535

数位dp题目:

UVa 11361

UVa 10712

POJ 3208

hdu 5435

hdu 4352

hdu 2089

hdu 3555

hdu 3709

hdu 3652

hdu 4507

hdu 4734

题解说明,有时间定当补上!

0 0
原创粉丝点击