算法初步:再讨论一点动态规划

原创 by zoe.zhang

  动态规划真的很好用啊,但是需要练习,还有很多技巧要学习。

1.滚动数组

  动态规划是用空间换取时间,所以通常需要为DP数组开辟很大的内存空间来存放数据,但有的时候空间太大超过内存限制,特别是在OJ的时候,容易出现MLE的问题。而在一些动规的题目中,我们可以利用滚动数组来优化空间。

  适用条件:DP状态转移方程中,仅仅需要前面若干个状态,而每一次转移后,都有若干个状态不会再被用到,也就是废弃掉,此时可以使用滚动数组来减少空间消耗。

  优点:节约空间。

  缺点:在时间消耗上没什么优势,有可能还带来时间上的额外消耗。在打印方案或者需要输出每一步的状态的时候比较困难,因为此时只有一个最终的状态结果。

举例:

1)一维数组:斐波那契数列

// 至少100个内存空间
int d[100] = { 0 };
d[0] = 1; d[1] = 1;
for (int i = 2; i < 100; i++)
    d[i] = d[i - 1] + d[i - 2];
cout << d[99];

// 滚动数组
int d[3] = { 0 };
d[0] = 1; d[1] = 1;
for (int i = 2; i < 100; i++)
    d[i%3] = d[(i - 1)%3] + d[(i - 2)%3];
cout << d[99];

 2) 二维数组:

// 至少100*100的内存空间
int dp[100][100];
for (int i = 1; i < 100; i++)
    for (int j = 1; j < 100; j++)
        d[i][j] = d[i - 1][j] + d[i][j - 1];

// 滚动数组
int dp[2][100];
for (int i = 1; i < 100; i++)
    for (int j = 1; j < 100; j++)
        d[i%2][j] = d[(i - 1)%2][j] + d[i%2][j - 1];

   注意这里取模%的操作是比较通用的操作,因为有些滚动数组可能有超过3维以上的维度,需要用到多个状态。

  不过我们在做动态规划的题目中,最常见用到的就是二维的滚动数组,所以我们可以用一些其他方法来代替取模操作(取模还是比较费时的)。可以用&1来代替%2的操作,也可以设置一个变量t,因为只有0和1两种状态,可以使用^来改变t的状态,也可以使用 t = 1-t 来变换t 的状态,这些比取模的操作都要快一些。

 

2.堆砖块

  【题目】小易有n块砖块,每一块砖块有一个高度。小易希望利用这些砖块堆砌两座相同高度的塔。为了让问题简单,砖块堆砌就是简单的高度相加,某一块砖只能使用在一座塔中一次。小易现在让能够堆砌出来的两座塔的高度尽量高,小易能否完成呢。 

  输入包括两行:第一行为整数n(1 ≤ n ≤ 50),即一共有n块砖块;第二行为n个整数,表示每一块砖块的高度height[i] (1 ≤ height[i] ≤ 500000)

  输出描述:如果小易能堆砌出两座高度相同的塔,输出最高能拼凑的高度,如果不能则输出-1;保证答案不大于500000。

  输入例子:3

        2 3 5
 输出例子:5

  【解答】:借鉴别人的答案,dp[i][j] 的值为较矮的那块砖的值,i值为第 i 块砖,j 为 两块砖之间的高度差,那么结果要求 d[n][0] 的值。

  状态的转移:a[i] 是第 i 块砖的高度

  (1)第 i 块砖不放: d[i][j] = d[i-1][j];

  (2)第 i 块砖放在矮的那堆上,高度依旧比原来高的矮:  d[i][j] = d[i-1][j+a[i]]+a[i]  (此时高度差为j; 原来的高度差为 j+ a[i])

  (3)第 i 块砖放在矮的那堆上,高度依旧比原来高的高: d[i][j] = d[i-1][a[i]-j]+a[i]-j (此时原来高的变为矮的,原来的高度差为 a[i]- j, 然后求原来高的高度)

  (4)第 i 块砖放在高的那堆上: d[i][j] = d[i-1][j-a[i]]

  初始化:d[0][i]=-1, d[0][0]=0。

  这里我们需要使用滚动数组,因为需要开辟的空间有50 *50 0000,很大,所以建议使用滚动数组来优化一下空间。

  【程序】

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxh = 500000 + 5;
int a[55];
int dp[2][maxh];  // 2维数组 

int main()
{
    int n;
    int sum = 0;
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        sum += a[i];  //这里有点疑问
    }

    memset(dp[0], -1, sizeof(dp[0]));  // 初始化边际条件
    dp[0][0] = 0;

    int t = 1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= sum; j++)
        {
            dp[t][j] = dp[t ^ 1][j];  //【1】  注意 t^1 
            if ((j + a[i] <= sum) && (dp[t ^ 1][j + a[i]] >= 0))   
                dp[t][j] = max(dp[t][j], dp[t ^ 1][j + a[i]] + a[i]);
            if ((a[i] - j >= 0) && (dp[t ^ 1][a[i] - j] >= 0))   // && 的短路操作
                dp[t][j] = max(dp[t][j], dp[t ^ 1][a[i] - j] + a[i] - j);
            if (j - a[i] >= 0 && dp[t ^ 1][j - a[i]] >= 0)
                dp[t][j] = max(dp[t][j], dp[t ^ 1][j - a[i]]);
        }
        t ^= 1;   // 也可以将 所有的 t^1 换成: t = 1- t
    }
    cout << (dp[t ^ 1][0] == 0 ? -1 : dp[t ^ 1][0]);

    return 0;
}

 

  另外一种滚动数组是采用的取模:%2 也就是& 1的操作

#include <iostream>  
#include <cstdio>  
#include <cstring>  
#include <algorithm>  
using namespace std;

const int maxh = 500000 + 5;
int a[55];
int d[2][maxh];

int main()
{
    int n;
    int sum = 0;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        sum += a[i];
    }
    memset(d[0], -1, sizeof(d[0]));
    d[0][0] = 0;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 0; j <= sum; j++)
        {
            d[i & 1][j] = d[(i - 1) & 1][j];
            if (j + a[i] <= sum && d[(i - 1) & 1][j + a[i]] >= 0)
                d[i & 1][j] = max(d[i & 1][j], d[(i - 1) & 1][j + a[i]] + a[i]);
            if (a[i] - j >= 0 && d[(i - 1) & 1][a[i] - j] >= 0)
                d[i & 1][j] = max(d[i & 1][j], d[(i - 1) & 1][a[i] - j] + a[i] - j);
            if (j - a[i] >= 0 && d[(i - 1) & 1][j - a[i]] >= 0)
                d[i & 1][j] = max(d[i & 1][j], d[(i - 1) & 1][j - a[i]]);
        }
    }
    printf("%d\n", d[n & 1][0] == 0 ? -1 : d[n & 1][0]);
    return 0;
}

 

3.最长回文子序列(Palindromic sequence)

  这里有几个概念最长公共子序列LCS,最长公共子串,最长回文子序列,最长回文子串,子序列通常要求元素之间不是连续,而子串要求元素之间必须是连续的。

  LCS是最经典的动态规划例题,LPS最长回文子序列也是经常用得到,这里做一点探讨。(LCS 在之前一篇博文已经讲过了,这里只讲一下LPS)

  求LPS有两种方法:

  1)假设有一个序列S,其最长回文子序列为Sp,其逆序序列是ST,则 LPS(S)= Sp = LCS(S,ST),就是说最长回文子序列就是序列S与其逆序ST之间的最长公共子序列,这个很好理解,在求解时套用LCS的解法即可。这个思想已经可以很好的解决LPS的问题,只有一单缺点,就是逆序操作可能需要消耗一点时间和空间。

  2)直接动态规划,列出状态转移方程。LPS(i,j)是 序列S(i,i+1,i+2,……j)的最长回文子序列

  

//动态规划求解最长回文子序列,时间复杂度为O(n^2)  
int lpsDp(char *str, int n)  
{  
    int dp[10][10], tmp;  
    memset(dp, 0, sizeof(dp));  
    for (int i = 0; i < n; ++i)  dp[i][i] = 1;  
  
    for (int i = 1; i < n; ++i)  
    {  
        tmp = 0;  
        //考虑所有连续的长度为i+1的子串,str[j....j+i]  
        for (int j = 0; j + i < n; j++)  
        {  
            if (str[j] == str[j + i])  
                tmp = dp[j + 1][j + i - 1] + 2;  
            else   
                tmp = max(dp[j + 1][j + i], dp[j][j + i - 1]);  
            dp[j][j + i] = tmp;  
        }  
    }  
    return dp[0][n - 1]; //返回字符串str[0...n-1]的最长回文子序列长度  
}  

例:poj1159 给你一个字符串,可在任意位置添加字符,最少再添加几个字符,可以使这个字符串成为回文字符串。(这一道用到了LPS和滚动数组。)

int FindLenLPS(int n)
{
    for (int i=0; i<2; ++i)
    {
        for (int j=0; j<n; ++j)
        {
            AnsTab[i][j]=0;
        }
    }
    AnsTab[0][0]=1;

    int refresh_col;    
    int base_col;    
    for (int i=1; i<n; ++i)    //从第一列开始,共需更新n-1列(次);自左向右
    {
        refresh_col=i%2;
        base_col=1-refresh_col;

        AnsTab[refresh_col][i]=1;

        for (int j=i-1; j>=0; --j)    //自下而上
        {
            //应用状态转移方程
            if (inSeq[j]==inSeq[i])
            {
                AnsTab[refresh_col][j]=2+AnsTab[base_col][j+1];
            }
            else
            {
                AnsTab[refresh_col][j]=max(AnsTab[refresh_col][j+1], AnsTab[base_col][j]);
            }
        }
    }

    return AnsTab[(n-1)%2][0];    //这就是LPS的长度
}

 

posted @ 2017-06-06 15:15  安安zoe  阅读(281)  评论(0编辑  收藏  举报