ACM-动态规划总结

来源:互联网 发布:seo 专家 顺丰 编辑:程序博客网 时间:2024/04/29 02:58

(欢迎阅读我的博客,如发现错误或有建议请评论留言,谢谢。)

动态规划是解决多阶段决策的一种方法,因此可以看出,动态规划要有多个类似阶段,对于每一段还要有相应的决策。

对于判断动态规划问题最关键的是要考虑问题的无后效性原则,否则的话,前后阶段之间不能很好的划分开,在这种情况下使用动态规划已求出的阶段还受到未知阶段的影响,就会影响结果的正确性。

当然动态规划还需要最优性原则和决策,但相对于无后效性原则比较容易判断。

根据前后阶段的关系和每一阶段的决策,就可以写出状态转移方程,这是动态规划问题的关键性的一步,之后根据转移方程写代码就比较容易了。


       在动态规划问题中,通常的(二维)动态规划问题往往要申请一个大小为n,m的二维数组,然后采用for循环来对每一阶段进行,这样的话空间消耗为(n*m)是比较大的,而对于n*m过大的问题,申请数组时往往会因过大而出错,这时候,就需要采用动态规划中的一个常见处理方法----"滚动数组",对于大部分动态规划问题可以大大降低空间消耗,具体方法用一个例子来说明

就用菲波那切数列问题来举例吧;

状态转移方程为:dp [i]=dp[i-1]+dp[i-2]

在这个转移方程中,我们用到了 i, i-1,i-2,一共三个阶段的值,其实对于所有的阶段,都只需要三个阶段的值,因此我们可以这么写

dp[i%3]=dp[(i-1)%3]+dp[(i-2)%3]               这就是滚动数组的取余

这样的话,如果要求第n(n很大)个数的值,我们并不需要定义长度为n的数组了,而只用定义数组dp[3],就可以实现了,这样就大大降低了空间的使用,这种如同滚动使用dp空间的方法就是滚动数组,当然,使用滚动数组也只是节省空间而已,对于时间并没有什么影响

上面的菲波那切数列是一维动态规划,那再举一个二维的例子

回文串问题:

回文串是一个正读和反读都一样的字符串,比如level或者noon等等就是回文串。给你一个长度为n( 3 <= N <= 1000. )的字符串。问最少需要插入几个字符,能够使它变成回文串
比如字符串 “Ab3bd”可以转换成(“dAb3bAd” or “Adb3bdA”). 但需要插入最少两个字符

一般做法的话申请dp[1001][1001]的空间进行动态规划,但是用到的空间太大了

根据状态转移方程

                if(a[i]==a[j])  

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

                else  

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

可以看出滚动的距离是2,因此改为滚动数组

申请dp[2][1001]

状态转移方程改为

                if(a[i]==a[j])
                    dp[i%2][j]=dp[(i+1)%2][j-1];
                else
                    dp[i%2][j]=1+mindp(dp[(i+1)%2][j],dp[i%2][j-1]);

使用滚动数组的回文串代码如下:


#include<bits/stdc++.h>
using namespace std;
int dp[2][1010];
int mindp(int a,int b)
{
    if(a<b) return a;
    else return b;
}
int main()
{
    int n,i,j;
    char a[1010];
    cin>>n;
    while(n--)
    {
        cin>>a;
        for(i=strlen(a)-1;i>=0;i--)
            for(j=i;j<=n;j++)
            {
                if(a[i]==a[j])
                    dp[i%2][j]=dp[(i+1)%2][j-1];
                else
                    dp[i%2][j]=1+mindp(dp[(i+1)%2][j],dp[i%2][j-1]);
            }
            cout<<dp[0][strlen(a)-1]<<endl;
    }
}


       其实背包问题中的那个伪代码也可以看做是使用了滚动数组,但是比较特殊,因为dp[i][v]=max(dp[i-1][v],dp[i-1][v-c[i]]+w[i])这里对于当前要求的即第i阶段的值,只与与i-1这一个阶段的值有关,所以可以直接把i这一维去掉(其实是优化,好像是没有了,但还是存在的),写成dp[v]=max(dp[v],dp[v-c[i]]+w[i])

当然,在for循环中i循环是不能去的,因为在转移方程中只是把i优化了,何况还有c[i],w[i]呢

       还有一点,对于背包问题的伪代码,01和完全以及多重背包是不一样的,01问题每类物品只能用一次,完全和多重可以多次使用(完全无限制使用,多重受限制使用,当然完全可多重的使用都受到背包容量的限制),所以对于01背包内层循环要用倒序(避免物品多次使用),对于完全背包和多重背包内层循环要(必须用)用正序,为的是使物体多次使用。

       01背包问题中还有一个关于内层循环的边界的常数优化,能够节省一点时间,但通常使用c[i]做边界值,也可以节省一点时间,效果有时比优化后的要差一点,详见《背包九讲》


       另一种动态规划特殊问题是二分法,当然背包问题也是一种特使情况,它们的使用都要有一定的条件,对于二分问题,使用的条件是要有单调性,然后用缩短区域的方法逐步确定所求值,还有一种二分问题的延伸,就是三分问题,与二分类似,不同就是三分就像抛物线一样存在一个拐点,做法类似。相比普通动态规划,二分法是很节省时间的。

0 0