动态规划

动态规划

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。

基本思想

若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

分治与动态规划

共同点:二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小(小到很容易解决的程序)的子问题.然后将子问题的解合并,形成原问题的解.

不同点:分治法将分解后的子问题看成相互独立的,通过用递归来做。

     动态规划将分解后的子问题理解为相互间有联系,有重叠部分,需要记忆,通常用迭代来做。

问题特征

最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。

重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解。

步骤

描述最优解的结构

递归定义最优解的值

按自底向上的方式计算最优解的值

由计算出的结果构造一个最优解

注意需要需要二维数组用容器,C++动态分配二维数组太坑爹

典型问题

01背包问题

背包九讲:http://www.cnblogs.com/jbelial/articles/2116074.html

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。

将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。 

int main()
{
    //int m = 120;
    //int n = 5;
    //vector<int> w = { 0, 40, 50, 70, 40, 20 };
    //vector<int> v = { 0, 10, 25, 40, 20, 10 };

    int m, n;    //m重量,n数量
    while (cin >> m >> n)
    {
        vector<int> w(n + 1, 0);
        vector<int> v(n + 1, 0);
        for (int i = 1; i <= n; i++)
        {
            int tmp;
            cin >> tmp;
            w[i] = tmp;
        }
        for (int i = 1; i <= n; i++)
        {
            int tmp;
            cin >> tmp;
            v[i] = tmp;
        }
        vector< vector<int> > vec(n + 1, vector<int>(m + 1, 0));
        for (int i = 1; i <= n; i++)
        {
            for (int j = 1; j <= m; j++)
            {
                if (w[i] > j)
                    vec[i][j] = vec[i - 1][j];
                else
                {
                    int tmp1 = v[i] + vec[i - 1][j - w[i]];
                    int tmp2 = vec[i - 1][j];
                    vec[i][j] = tmp1 > tmp2 ? tmp1 : tmp2;
                }
            }
        }
        double val = vec[n][m] * 0.1;
        cout << val << endl;
    }

    system("pause");
}

 

最长公共子序列(不连续) LCS  Longest Common Subsequence

找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。而最长公共子序列则并不要求连续。

cnblogs与belong,最长公共子序列为blog(cnblogs, belong),最长公共子串为lo(cnblogs, belong)

这两个问题都是用空间换空间,创建一个二维数组来记录之前的每个状态

参考:【动态规划】最长公共子序列与最长公共子串

C++实现最长公共子序列和最长公共子串

状态转移方程:

用i,j遍历两个子串x,y,如果两个元素相等就+1 ,不等就用上一个状态最大的元素

 1 public static int lcs(String str1, String str2) {
 2     int len1 = str1.length();
 3     int len2 = str2.length();
 4     int c[][] = new int[len1+1][len2+1];
 5     for (int i = 0; i <= len1; i++) {
 6         for( int j = 0; j <= len2; j++) {
 7             if(i == 0 || j == 0) {
 8                 c[i][j] = 0;
 9             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
10                 c[i][j] = c[i-1][j-1] + 1;
11             } else {
12                 c[i][j] = max(c[i - 1][j], c[i][j - 1]);
13             }
14         }
15     }
16     return c[len1][len2];
17 }

最长公共子串(连续)

状态转移方程:

区别就是因为是连续的,如果两个元素不等,那么就要=0了而不能用之前一个状态的最大元素

 1 public static int lcs(String str1, String str2) {
 2     int len1 = str1.length();
 3     int len2 = str2.length();
 4     int result = 0;     //记录最长公共子串长度
 5     int c[][] = new int[len1+1][len2+1];
 6     for (int i = 0; i <= len1; i++) {
 7         for( int j = 0; j <= len2; j++) {
 8             if(i == 0 || j == 0) {
 9                 c[i][j] = 0;
10             } else if (str1.charAt(i-1) == str2.charAt(j-1)) {
11                 c[i][j] = c[i-1][j-1] + 1;
12                 result = max(c[i][j], result);
13             } else {
14                 c[i][j] = 0;
15             }
16         }
17     }
18     return result;
19 } 

KMP

KMP算法

硬币找零问题

假设有几种硬币,如1 5 10 20 50 100,并且数量无限。请找出能够组成某个数目的找零所使用最少的硬币数。

解法:

用待找零的数值k描述子结构/状态,记作sum[k],其值为所需的最小硬币数。对于不同的硬币面值coin[0...n],有sum[k] = min(sum[k-coin[0]] , sum[k-coin[1]], ...)+1。对应于给定数目的找零total,需要求解sum[total]的值。

注意要从前往后算,从后往前算无法保存状态,需要递归,效率很低,就不是动态规划了

#include <iostream>
using namespace std;

#define MaxNum  pow(2,31) - 1

int main()
{
    int n;
    while (cin >> n)
    {
        vector<int> c(n + 1, 0);
        for (int i = 1; i <= n; i++)
        {
            if (i == 1 || i == 5 || i == 10 || i == 20 || i == 50 || i == 100)
            {
                c[i] = 1;
                continue;
            }
            int curMin = MaxNum;
            if (i - 1 > 0)
                curMin = c[i - 1] < curMin ? c[i - 1] : curMin;
            if (i - 5 > 0)
                curMin = c[i - 5] < curMin ? c[i - 5] : curMin;
            if (i - 10 > 0)
                curMin = c[i - 10] < curMin ? c[i - 10] : curMin;
            if (i - 20 > 0)
                curMin = c[i - 20] < curMin ? c[i - 20] : curMin;
            if (i - 50 > 0)
                curMin = c[i - 50] < curMin ? c[i - 50] : curMin;
            if (i - 100 > 0)
                curMin = c[i - 100] < curMin ? c[i - 100] : curMin;
            c[i] = curMin + 1;
        }
        cout << c[n] << endl;
    }
    

    system("pause");
    return 0;
}
View Code

 

类似硬币的问题找平方个数最小

题目:

给一个正整数 n, 找到若干个完全平方数(比如1, 4, 9, ... )使得他们的和等于 n。你需要让平方数的个数最少。
给出 n = 12, 返回 3 因为 12 = 4 + 4 + 4。
给出 n = 13, 返回 2 因为 13 = 4 + 9。
#include<iostream>  
using namespace std;

int findMin(int n)
{
    int *result = new int(n + 1);
    result[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        int minNum = i;
        for (int j = 1;; j++)
        {
            if (i >= j * j)
            {
                int tmp = result[i - j*j] + 1;
                minNum = tmp < minNum ? tmp : minNum;
            }
            else
                break;
        }
        result[i] = minNum;
    }
    return result[n];
}

int main()
{
    int n;
    while (cin >> n)
        cout << findMin(n) << endl;

}
View Code

最长回文字串

参考:最长回文子串

回文字符串的子串也是回文,比如P[i,j](表示以i开始以j结束的子串)是回文字符串,那么P[i+1,j-1]也是回文字符串。这样最长回文子串就能分解成一系列子问题了。这样需要额外的空间O(N^2),算法复杂度也是O(N^2)。

首先定义状态方程和转移方程:

P[i,j]=0表示子串[i,j]不是回文串。P[i,j]=1表示子串[i,j]是回文串。

P[i+1][j-1]&&s.at(i)==s.at(j)

初始化是准备两个元素是回文的情况aa,bb

 1 string findLongestPalindrome(string &s)
 2 {
 3     const int length=s.size();
 4     int maxlength=0;
 5     int start;
 6     bool P[50][50]={false};
 7     for(int i=0;i<length;i++)//初始化准备
 8     {
 9         P[i][i]=true;
10         if(i<length-1&&s.at(i)==s.at(i+1))
11         {
12             P[i][i+1]=true;
13             start=i;
14             maxlength=2;
15         }
16     }
17     for(int len=3;len<length;len++)//子串长度
18         for(int i=0;i<=length-len;i++)//子串起始地址
19         {
20             int j=i+len-1;//子串结束地址
21             if(P[i+1][j-1]&&s.at(i)==s.at(j))
22             {
23                 P[i][j]=true;
24                 maxlength=len;
25                 start=i;
26             }
27         }
28     if(maxlength>=2)
29         return s.substr(start,maxlength);
30     return NULL;
31 }

最长递增序列

问题:设L=<a1,a2,…,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lin=<aK1,ak2,…,akm>,其中k1<k2<…<km且aK1<ak2<…<akm。求最大的m值。

第一种方法,排序,然后用LCS来解决:设序列X=<b1,b2,…,bn>是对序列L=<a1,a2,…,an>按递增排好序的序列。那么显然X与L的最长公共子序列即为L的最长递增子序列。这样就把求最长递增子序列的问题转化为求最长公共子序列问题LCS了。

第二种:时间复杂度O(N^2)的算法:

LIS[i]:表示数组前i个元素中(包括第i个),最长递增子序列的长度

LIS[i] = max{ LIS[i] , LIS[k]+1 }, 0 <= k < i, a[i]>a[k]

LIS数组的值表示前i个元素的最长子序列。i从第一个元素到最后一个元素遍历一遍,j从第一个元素到第i个元素遍历,如果第i个元素大于j,并且LIS[J] + 1比LIS[I]还大就更新,相当于把j加入到这个递增序列了

 1 int LIS(int a[], int length)
 2 {
 3     int *LIS = new int[length];
 4     for(int i = 0; i < length; ++i)
 5     {
 6         LIS[i] = 1; //初始化默认长度
 7         for(int j = 0; j < i; ++j) //前面最长的序列
 8             if(a[i] > a[j] && LIS[j]+1 > LIS[i])
 9                 LIS[i] = LIS[j]+1;  
10     }
11     int max_lis = LIS[0];
12     for(int i = 1; i < length; ++i)
13         if(LIS[i] > max_lis)
14             max_lis = LIS[i];
15     return max_lis;  //取LIS的最大值
16 }

 

字符串相似度/编辑距离(edit distance)

N皇后问题

其他问题

1.某幢大楼有100层。你手里有两颗一模一样的玻璃珠。当你拿着玻璃珠在某一层往下扔的时候,一定会有两个结果,玻璃珠碎了或者没碎。这幢大楼有个临界楼层。低于它的楼层,往下扔玻璃珠,玻璃珠不会碎,等于或高于它的楼层,扔下玻璃珠,玻璃珠一定会碎。玻璃珠碎了就不能再扔。现在让你设计一种方式,使得在该方式下,最坏的情况扔的次数比其他任何方式最坏的次数都少。也就是设计一种最有效的方式。

例如:有这样一种方式,第一次选择在60层扔,若碎了,说明临界点在60层及以下楼层,这时只有一颗珠子,剩下的只能是从第一层,一层一层往上实验,最坏的情况,要实验59次,加上之前的第一次,一共60次。若没碎,则只要从61层往上试即可,最多只要试40次,加上之前一共需41次。两种情况取最多的那种。故这种方式最坏的情况要试60次。仔细分析一下。如果不碎,我还有两颗珠子,第二颗珠子会从N+1层开始试吗?很显然不会,此时大楼还剩100-N层,问题就转化为100-N的问题了。

那该如何设计方式呢?

根据题意很容易写出状态转移方程:N层楼如果从n层投下玻璃珠,最坏的尝试次数是:clip_image002[6]

那么所有层投下的最坏尝试次数的最小值即为问题的解:clip_image002[8]。其中F(1)=1.

 1 /*
 2 *侯凯,2014-9-15
 3 *功能:100楼层抛珠问题
 4 */
 5 #include<iostream>
 6 using namespace std;
 7 
 8 int max(int a, int b)
 9 {
10     return (a > b)? a : b;
11 }
12 
13 int dp[101];
14 //N<=100;
15 int floorThr(int N)
16 {
17     for (int i = 2; i <= N; i++)
18     {
19         dp[i] = i;
20         for (int j = 1; j<i; j++)
21         {
22             int tmp = max(j, 1 + dp[i - j]);    //j的遍历相当于把每层都试一遍
23             if (tmp<dp[i])
24                 dp[i] = tmp;
25         }
26     }
27     return dp[N];
28 }
29 
30 int main()
31 {
32     dp[0] = 0;
33     dp[1] = 1;
34     int dis = floorThr(100);
35     cout << dis << endl;
36     system("Pause");
37 }

输出为14,说明在合适的楼层抛玻璃珠,最差情况下只需14次可找到临界层。

答案是先从14楼开始抛第一次;如果没碎,再从27楼抛第二次;如果还没碎,再从39楼抛第三次;如果还没碎,再从50楼抛第四次;如此,每次间隔的楼层少一层。这样,任何一次抛棋子碎时,都能确保最多抛14次可以找出临界楼层。

N*N方格内的走法问题

 1 #include<iostream>
 2 #include<vector>
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     int n;
 8     while (cin >> n)
 9     {
10         vector<vector<int>> dp(n+1, vector<int>(n+1, 1));
11         for (int i = 1; i <= n;i++)
12         {
13             for (int j = 1; j <= n;j++)
14             {
15                 dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
16             }
17         }
18         cout << dp[n][n] << endl;
19     }
20 }

 

其他问题参考:

http://www.cnblogs.com/wuyuegb2312/p/3281264.html#q1a1

http://www.cnblogs.com/luxiaoxun/archive/2012/11/15/2771605.html

posted on 2016-08-15 10:49  已停更  阅读(61959)  评论(2编辑  收藏  举报