DP总结

DP使用规则(摘抄自http://www.cnblogs.com/huangxincheng/archive/2012/02/13/2349664.html):

①  最优化原理(最优子结构性质):

     如果一个问题的最优策略它的子问题的策略也是最优的,则称该问题具有“最优子结构性质”。

②  无后效性:

     当一个问题被划分为多个决策阶段,那么前一个阶段的策略不会受到后一个阶段所做出策略的影响。

③  子问题的重叠性:

     这个性质揭露了动态规划的本质,解决冗余问题,重复的子问题我们可以记录下来供后阶段决策时

     直接使用,从而降低算法复杂度。

 

求解步骤:

①   描述最优解模型。

②   递归的定义最优解,也就是构造动态规划方程。

③   自底向上的计算最优解。

④   最后根据计算的最优值得出问题的最佳策略。

 

DP归类:

一、01背包:时间复杂度O(N*V),N表示N个物品,V表示背包重量

for i=1..N
for j=V..0
首先想想为什么01背包中要按照v=V..0的逆序来循环。这是因为要保证第i次循环中的状态f[v]是由状态f[v-c]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个没有已经选入第i件物品的子结果f[v-c]。

二、完全背包:O(N*V)个状态需要求解,但求解每个状态f[v]的时间是O(V/c),总的复杂度是超过O(VN)的。

for i=1..N
for j=0..V
初始化的细节问题我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。
有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别
这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,
这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设
为0。
注意:过多的赋值运算和调用函数会增加时间,对于一些BT的题目,提倡用if() X=Y这样的方式,而不推荐X=max(X,Y);

三、最长公共子序列

int dp[1005][1005];//可以使用动态数组优化内存
char a[1005],b[1005],re[1005][1005];
void dfs(int i,int j)
{
    if(i<1||j<1) return;
    if(re[i][j]=='c')
   {        
       dfs(i-1,j-1);
       cout<<a[i];
    }
    else dfs(i-(re[i][j]=='x'),j-(re[i][j]=='y'));
}
int main()
{
 while(~scanf("%s %s",a+1,b+1))
 {
     int la=strlen(a+1),lb=strlen(b+1),i,j;
     memset(dp,0,sizeof(dp));
     //printf("%s\n",a+1);
     for(i=1;i<=la;++i)
         for(j=1;j<=lb;++j)
         {
             if(a[i]==b[j])
             {
                 dp[i][j]=dp[i-1][j-1]+1;
                 re[i][j]='c';
             }
             else {
                 if(dp[i][j-1]>dp[i-1][j])
                 {
                     dp[i][j]=dp[i][j-1];
                     re[i][j]='y';
                 }
                 else{
                     dp[i][j]=dp[i-1][j];
                     re[i][j]='x';
                 }
             }
         }
         printf("%d\n",dp[la][lb]);
         dfs(la,lb);
         printf("\n");
 }
 return 0;
}

 四、经典的数塔问题:

从上往下、从下往上都可以:从上往下代码更简洁

上——下:

int dp[105][105];
int main()
{
  int n,i,j;
  while(~scanf("%d",&n))
  {
      memset(dp,0,sizeof(dp));
      for(i=1;i<=n;++i)
          for(j=1;j<=i;++j)
          {
              scanf("%d",&dp[i][j]);
              dp[i][j]+=dp[i-1][j-1]>dp[i-1][j]?dp[i-1][j-1]:dp[i-1][j];//important
          }
          printf("%d\n",*max_element(dp[n],dp[n]+n));
  }
 return 0;
}

下——上:

int main()
{
  int t,n,a[105][105],i,j;
  while(~scanf("%d",&n))
  {
      for(i=0;i<n;++i)
          for(j=0;j<=i;++j)
              scanf("%d",&a[i][j]);
      for(i=n-1;i>0;--i)
          for(j=0;j<i;++j)
          {
              a[i-1][j]+=a[i][j]>a[i][j+1]?a[i][j]:a[i][j+1];
          }
          printf("%d\n",a[0][0]);
  }
 return 0;
}        

 五、最大和问题:

1、一维:

for(i=0;i<n;++i)
      {
          scanf("%d",&a);
          if(sum<0) sum=a; //如果前面的数已经小于0了,重新记录sum
          else sum+=a;
          Max=sum>Max?sum:Max;
      }

2、二维:

 for(i=1;i<=r;++i) //r为行,c为列
          for(j=0;j<c;++j)
          {
              scanf("%d",&map[i][j]);
              map[i][j]=map[i][j]+map[i-1][j];//竖着降维
          }
      for(i=1,m=map[1][0];i<=r;++i) //m赋值给第一个元素
          for(j=i;j<=r;++j) 
           for(k=max=0;k<c;++k)
           {
               temp=map[j][k]-map[i-1][k];//k时,i和j之间的矩阵
               max=(max>=0?max:0)+temp;//一维的做法,max记录的是当前两行之间的最大矩阵和
               m=max>m?max:m;//m记录的是从前面遍历到这个位置,最大的矩阵和为多少
           }
           printf("%d\n",m);
           memset(map,0,sizeof(map));//放后面更省时间

3、三维:

 可以推广到N维:

#define FOR(i,s,t) for(int i=(s);i<=(t);++i)
void expand(int i,int &b0,int &b1,int &b2)
{
    b0=i&1;
    i>>=1;
    b1=i&1;
    i>>=1;
    b2=i&1;
}
int sign(int b0,int b1,int b2)
{
    return (b0+b1+b2)%2==1?1:-1;
}
const int maxn=30;
const long long INF=1LL<<60;
long long S[maxn][maxn][maxn];
long long sum(int x1,int x2,int y1,int y2,int z1,int z2)
{
    int dx=x2-x1+1,dy=y2-y1+1,dz=z2-z1+1;
    long long s=0;
    for(int i=0;i<8;++i)
    {
        int b0,b1,b2;
        expand(i,b0,b1,b2);
        s-=S[x2-b0*dx][y2-b1*by][z2-b2*dz]*sign(b0,b1,b2);
    }
    return s;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int a,b,c,b0,b1,b2;
        scanf("%d%d%d",&a,&b,&c);
        memset(S,0,sizeof(S));
        FOR(x,1,a)
        FOR(y,1,b)
        FOR(z,1,c) 
        scanf("%lld",&S[x][y][z]);
        FOR(x,1,a) FOR(y,1,b) FOR(z,1,c) FOR(i,1,7)
        {
            expand(i,b0,b1,b2);
            S[x][y][z]+=S[x-b0][y-b1][z-b2]*sign(b0,b1,b2);
        }
        long long ans =-INF;
        FOR(x1,1,a) FOR(x2,x1,a) FOR(y1,1,b) FOR(y2,y1,b)
        {
            long long M=0;
            FOR(z,1,c)
            {
                long long s=sum(x1,x2,y1,y2,1,z);
                ans=max(ans,s-M);
                M=min(M,s);
            }
        }
        printf("%lld\n",ans);
        if(t) printf("\n");
    }
    return 0;
}

 六、整数拆分:

/*
第一行:将n划分成若干正整数之和的划分数。
状态转移方程:dp[i][j]:和为i、最大数不超过j的拆分数
dp[i][j]可以分为两种情况:1、拆分项至少有一个j 2、拆分项一个j也没有
dp[i][j]=dp[i-j][[j]+dp[i][j-1]

第二行:将n划分成k个正整数之和的划分数。
dp[n-k][k]:相当于把k个1从n中拿出来,然后和n-k的拆分项相加的个数

第三行:将n划分成若干最大不超过k的正整数之和的划分数。
dp[n][k]

第四行:将n划分成若干奇正整数之和的划分数。
dp1[i][j]是当前的划分数为i,最大值为j时的中的划分数,则状态转移方程为
dp1[i][j]=dp1[i][i]if(j>i&&j%2==1)
=dp1[i][i-1]if(j>i&&j%2==0)(最大数不可能为偶数)
=dp1[i-j][j]+dp1[i][j-2]没用到j时划分不变,即dp1[i][j-2],用到则是dp1[i-j][j];

第五行:将n划分成若干完全不同正整数之和的划分数。
dp2[i][j]可以分两种情况:1、dp1[i][j-1]为不选择j时的方案  2、dp1[i-j][j-1]为选择j时的方案
0-1背包:dp2[i][j]=dp2[i][j-1]+dp2[i-j][j-1]

第六行:将n划分成不超过k个正整数之和的划分数。
dp[n][k]: ferrers共轭图像
比如 24=5+5+5+4+3+2,6个数,最大数为5
     24=6+6+5+4+3 5个数,最大数为6
*/
#include <stdio.h>
#define N 52
int dp[N][N] = {0} , dp1[N][N] = {0}  , dp2[N][N] = {0} ;
void  Divid(){
      dp[0][0] = 1 ;
      for( int i = 0 ; i < N ; i++ )
            for( int j = 1 ; j < N ; j++ ){
                  if( i < j ) //当i<j时 只能j分到i而已,所以情况有1种
                        dp[i][j] = dp[i][i] ;
                  else
                        dp[i][j] = dp[i-j][j] + dp[i][j-1] ;
            }
}
void Divid1(){
      for( int i = 1 ; i < N ; i++ ) //各种初始化
            dp1[i][1] = 1 ;           //最大值为1时只能有本身组成,Answer为1
      for( int i = 1 ; i < N ; i+=2 )
            dp1[0][i] = 1 ;
      dp1[0][0] = 1 ;
      for( int i = 1 ; i < N ; i++ )
            for( int j = 3 ; j < N ; j+=2 ){ //j是永远的奇数
                  if( j > i ){
                        if( i&1 )
                              dp1[i][j] = dp1[i][i] ;
                        else
                              dp1[i][j] = dp1[i][i-1] ;
                  }
                  else
                        dp1[i][j] = dp1[i-j][j] + dp1[i][j-2] ;
            }
}
void Divid2(){
      for( int i = 1 ; i < N ; i++ ){
            dp2[0][i] = 1 ;
            dp2[1][i] = 1 ;
      }
      for( int i = 2 ;i < N ; i++ )
            for( int j = 1 ;j < N ; j++ ){
                  if( j > i )
                        dp2[i][j] = dp2[i][i] ;
                  else
                        dp2[i][j] = dp2[i-j][j-1] +dp2[i][j-1] ; //01背包
            }
}
int main(){
      int n , k ;
      Divid() ;
      Divid1() ;
      Divid2() ;
      while( ~scanf( "%d%d" , &n , &k ) )
      {
            printf( "%d\n" , dp[n][n] ) ; //拆分成若干个正整数        
            printf( "%d\n" , dp[n-k][k] ) ; //拆分成k个正整数
            printf( "%d\n" , dp[n][k]) ;  //拆分成最大数为k
            printf( "%d\n" , n&1 ? dp1[n][n] : dp1[n][n-1] ) ; //拆分成若干个奇数
            printf( "%d\n\n" , dp2[n][n]) ; //拆分成若干个不同的正整数
      }
      return 0;
}

Cut the rope:求解把长度为L的绳子分成至少2段的不同长度的绳子的方案数:

首先考虑,若分解成k段,则n的值至少为1+2+3+4+...+k=(k+1)*k/2

所以本题k的最大值为315

假定dp[k][n]表示为可以分成k段和为n的方案数,

情况分为两种:

1、只有一个1的,则等于dp[k-1][n-k],相当与从n里拿走k个1,可以分成k-1段的方案数

2、没有1的,则等于dp[k][n-k],相当于从n里拿走k个1,可以分成k段的方案数

所以 dp[k][n]=dp[k-1][n-k]+dp[k][n-k]

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<string>
using namespace std;
#define LL long long 
#define N 50005
#define mod 1000000
int dp[2][N],sum[N]; //dp滚动数组
int main()
{
 int n,t,i,j,k;
 memset(sum,0,sizeof(sum));
  /*这里dp[0]表示3开始后才可以分,如果想象成每段加1,则会有重复的
  想象成每段+2,则dp[0][i]=dp[0][i-2]+1不会有重复的*/
 for(i=3;i<=N;++i) //当绳子长度至少为3时,才有解噢
     sum[i]=dp[0][i]=dp[0][i-2]+1;
 for(k=3;k<316;++k)
 {
    int *p1=dp[k&1],*p2=dp[(k+1)&1]; //用数组指针指向dp数组,这样很方便噢
    for(i=k*(k+1)/2-1;i>0;i--) p1[i]=0; //由于之前可能用过,所以必须要重新清0,不然会WA(通常滚动数组都要重新清0)
    for(i=k*(k+1)/2;i<=N;++i)
    {
        p1[i]=p1[i-k]+p2[i-k];  //状态转移方程
        p1[i]=p1[i]>=mod?p1[i]-mod:p1[i]; //对mod求余
        sum[i]+=p1[i];
        sum[i]=sum[i]>=mod?sum[i]-mod:sum[i]; //对mod求余
    }
 }
 scanf("%d",&t);
 while(t--)
 {
     scanf("%d",&n);
     printf("%d\n",sum[n]);
 }
 return 0;
}

 

 七、深入浅出切汉诺塔之类的状态题:

题目1:用2*1或1*2的骨牌把m*n的棋盘完全覆盖,求覆盖方案数:

1、当高度(h)和宽度(w)为奇数时:

area=h*w;

骨牌面积:2

h*w / 2!=0  -> 不能用骨牌覆盖

2、记f[i][s1]为第i-1行全满且第i行状态为s1时的种数,f[i-1][s2]为第i-2行全满且第i-1行状态为s2时的种数,则骨牌的覆盖方案数会等于f[h][w<<1-1](第h行全满状态):

结论:第i行的放置方案数,取决于第i-1行的放置方案数

对于每一个位置,3种放置方案:

 1. 竖直放置

 2. 水平放置

 3. 不放置

d为当前列号 ,初始化d, s1, s2都为0;对应以上三种放置方法,s1, s2的调整为:

1. d = d + 1, s1 << 1 | 1, s2 << 1;

2. d = d + 2, s1 << 2 | 3, s2 << 2 | 3;

3. d = d + 1, s1 << 1,   s2 << 1 | 1;

先就第一种情况解释一下,另外的两种情况可以类推

S1<<1|1即为把s1的二进制表示后面加上一个1,对就于本题来说就是(d+1)列上放

置,s2<<1即为把s2的二进制表示后面加上一个0,对于本题来说就是(d+1)列上不放置。

但为什么s1、s2会如此变化呢?s1对应于本行的状态,s2对应于上一行的状态,能竖直放置意味着上一行的(d+1)列是空着的,因此此时上一行的状态为s2<<1,同时竖置放置了之后,则本行(d+1)行放置了东西,状态于是变为s1<<1|1;

3、初始时的f值,可以假设第0行全满,第一行只有两种放法:

 1. 水平放置 d = d + 2, s << 2 | 3;
 2. 不放置   d = d + 1, s << 1;

# include <stdio.h>
# include <string.h>
#include<iostream>
using namespace std;
long long a[2][3000];
int t,n,m;
void dfs(int d,int mm) //d列号 
{
if(d==m) 
{
   a[0][mm]++;//使用了滚动数组,当然是a[0]咯
   return;
}
if(d+1<=m)
   dfs(d+1,mm<<1);
if(d+2<=m)
   dfs(d+2,mm<<2|3);
}
void DFS(int d,int mm,int nn)//mm对应于上一行状态,nn对应于下一行状态
{
if(d==m)
{
   a[t][nn]+=a[(t+1)%2][mm];
   return;
}
if(d+1<=m) //判断防越界
{
   DFS(d+1,mm<<1,nn<<1|1); //第一种放置
   DFS(d+1,mm<<1|1,nn<<1); //第三种放置
}
if(d+2<=m) 
   DFS(d+2,mm<<2|3,nn<<2|3); //第二种放置
} 
int main()
{
int i;
while(scanf("%d%d",&n,&m)&&n&&m)
{
   if((n*m)%2)
   {
    printf("0\n");
    continue;
   }
   if(n>m) //当输入的 w > h 时,对调,因为横向的运算是指数级的, 而列向的是线性的.
    n^=m,m^=n,n^=m; //相当于swap(n,m)
   memset(a,0,sizeof(a));
   dfs(0,0);  //初始化第一行
   for(i=2;i<=n;i++)
   {
    t=(i+1)%2;
    DFS(0,0,0);
    memset(a[(t+1)%2],0,sizeof(a[0]));
   }
   cout<<a[(n+1)%2][(1<<m)-1]<<endl;
} 
return 0;
}

 

posted @ 2013-05-01 14:37  小仪在努力~  阅读(586)  评论(0编辑  收藏  举报