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

原创 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的长度
}
时间: 2024-11-06 18:18:35

算法初步:再讨论一点动态规划的相关文章

算法学习 - 01背包问题(动态规划C++)

动态规划 01背包 问题描述 求解思路 代码实现 放入哪些物品 代码 动态规划 我在上一篇博客里已经讲了一点动态规划了,传送门:算法学习 - 动态规划(DP问题)(C++) 这里说一下,遇到动态规划应该如何去想,才能找到解决办法. 最主要的其实是要找状态转移的方程,例如上一篇博客里面,找的就是当前两条生产线的第i个station的最短时间和上一时刻的时间关系. minTime(station[1][i]) = minTime(station[1][i-1] + time[i], station[

算法初步:快速乘,快速幂,矩阵快速幂

原创 by zoe.zhang 在刷题的时候遇到了问题,就是当循环或者递推的次数非常大的情况下获取一定结果,这个时候如果作普通运算,那么很容易就超时了,而且有时候结果也大得超范围了,即使是long long类型的也放不下,然后给了提示说是运用快速幂的思想.所以这里对快速幂做了一点思考和探讨. 1.快速乘,快速幂,矩阵快速幂三者的关系 不管是快速乘,还是快速幂算法,实际上都包含了分解问题的思想在里面,将O(n)的复杂度降到O(lgn).学习的时候,一般学习快速幂算法,再由此推广去解决矩阵快速幂问题

[从头学数学] 第174节 算法初步

剧情提要: [机器小伟]在[工程师阿伟]的陪同下进入了结丹中期的修炼, 这次要修炼的目标是[算法初步]. 正剧开始: 星历2016年04月12日 08:54:58, 银河系厄尔斯星球中华帝国江南行省. [工程师阿伟]正在和[机器小伟]一起研究[算法初步]. [人叫板老师]指点小伟说:"这金丹要想大成,顺利进入元婴期,就必须进行九转培炼. 这什么是九转培炼法门呢?就是要先快速的修炼[天地人正册]进入后期,不要管各种辅修 功法,然后从头游历[天地人列国],在游历中增长见闻,精炼神通,最后再修炼[术.

算法重拾之路——动态规划基础

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 闲谈: 快到期末了,身为计算机专业大三学生,各种大作业缠身,到了大三,不再局限于考试了,学校更重视的是实践能力,所以各种大作业啊,并行计算.软件工程.软件综合设计 等等,有些东西,要求做的,在前两年,没有学过,于是又去学习,基本要把时间榨干了,这本书看了很多,就

五大算法:分治,贪心,动态规划,回溯,分支界定

分治算法 一.基本概念 在计算机科学中,分治法是一种很重要的算法.字面上的解释是"分而治之",就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题--直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并.这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)-- 任何一个可以用计算机求解的问题所需的计算时间都与其规模有关.问题的规模越小,越容易直接求解,解题所需的计算时间也越少.例如,对于n个元素的排序问题,

1128: 零起点学算法35——再求多项式(含浮点)

1128: 零起点学算法35--再求多项式(含浮点) Time Limit: 1 Sec  Memory Limit: 64 MB   64bit IO Format: %lldSubmitted: 2141  Accepted: 1002[Submit][Status][Web Board] Description 输入一个整数n,计算 1+1/(1-3)+1/(1-3+5)+...+1/(1-3+5-...+2n-1)的值 Input 输入一个整数n(多组数据) Output 出1+1/(1

1097:零起点学算法04——再模仿一个算术题

1097: 零起点学算法04--再模仿一个算术题 Time Limit: 1 Sec  Memory Limit: 128 MB   64bit IO Format: %lldSubmitted: 2627  Accepted: 2202[Submit][Status][Web Board] Description 上题会模仿了吧.再来模仿一个. 现在要求你模仿一个乘法的算术题 Input 没有输入 Output 输出9乘以10的值 Sample Output 90 Source 零起点学算法

走快点,再快一点

最近的小目标有点多,一个人走得更快,趁着还是一个人走快点,再快一点. 1.设计模式要尽快,看完设计模式后找一些例子来巩固设计模式. 2.JDK“不常用”的API,反射.类加载器.动态代理.RMI等等. //做完以上两步后,开始研读开源框架源代码,从MyBatis3.4.1着手. 3.翻译MyBatis3.4.1官方用户手册. 4.着手研究Java多线程,多方面深入了解,不仅从Java本身,从操作系统以及计算机组成原理等底层方面来理解和运用.

Android图表库MPAndroidChart(七)—饼状图可以再简单一点

Android图表库MPAndroidChart(七)-饼状图可以再简单一点 接上文,今天实现的是用的很多的,作用在统计上的饼状图,我们看下今天的效果 这个效果,我们实现,和之前一样的套路,我先来说下这个的应用场景,假设,我是一名小学老师,现在教务处让我设置一个图表,说明下我带的班级期末考试有多少人优秀,多少人及格和不及格等等,而这些呢,我已经算出来百分比了,只剩下画图了,那好,我们就来实现以下吧 一.基本实现 首先是我们的布局 <com.github.mikephil.charting.cha