疯狂 dp(一) 线性 dp

来源:互联网 发布:php判断会员登录 编辑:程序博客网 时间:2024/05/21 18:54

     动态规划是为了求解一种包含大量重叠子问题的最优化问题的方法。基本思想是,将原问题分解为若干相似的子问题,在求解的过程中通过子问题的解求出原问题的解。听起来和分治法很相似,但是,分治法只是不断地将问题分解成小问题求解,而动规之所以优秀是它会进行类似于记忆化搜索的过程,在求解的过程中把每一个子问题的解保存下来(不管后面会不会用到),然后在求解更大的问题时,从这些子问题的解里面寻求当前问题的最优解。所以,从这里也可以看出动规解法的限定条件是,该问题有一个最优子结构,就是由它分解出的子问题的解也是最优的。

   

    这样看起来是很抽象的,我们接下来一个个模型看下去吧。感觉对 dp 不太好分类,个人就做简单的整理,代码不对或是分类有问题请大家指出,谢谢!

    首先是线性动规。


     单调递增最长子序列

     这个问题的基本模型是,给一个序列,要求它的最长递增子序列,子序列的值是递增的,它的坐标在原序列中也是递增的,但不要求连续。

     如,给一串数字, 0 2 3 4 3 4 6  9 13 24 11 26,那么它的单调递增最长子序列就是 0 2 3 4 6 9 13 24 26

     给一串字母,a f r e f j l m o d, 单调递增最长子序列就是 a e f j l m o


     下面是求一个字母序列的单调递增子序列 

     原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=17


     解1:

     输入字符串为 str, 用一个数组 dp 记录问题的解, dp[i] 表示以 str[i] 结束的单调递增最长子序列的长度。 

     状态转移方程: dp[i] = max(1, dp[j] + 1)  , str[j] < str[i]

     复杂度为 O(n^2)

      代码:    

#include <iostream>#include <string>#include <cstring>using namespace std;#define max(a,b) (a)>(b)?(a):(b)int main(){int dp[10001],ans;int T;cin>>T;while(T--){ans = 1;string str;cin>>str;int n = str.size();for(int i = 0;i < n;++i){dp[i] = 1;for(int j = 0;j < i;++j){if(str[i] > str[j]){dp[i] = max(dp[i],dp[j] + 1);if(dp[i] > ans) ans = dp[i];}}}cout<<ans<<endl;}return 0;}

    解二:

    数组 dp 存不是答案子序列,但它和子序列长度是一样的,因为它的不断更新,所以总能确保答案正确,多看几遍。复杂度大概只有 O(100*N),因为最长单调递增子序列的长度是不可能超过127的。

#include<iostream>#include<string>#include<cstring>using namespace std;//求最长单调递增子序列长度int length(string str){int dp[128], ans = 1;;memset(dp,0,sizeof(dp));int n = str.size();dp[0] = str[0]; //初始化for(int i = 0;i < n;++i){for(int j = ans - 1;j >= 0; --j){if(j == 0 && dp[0] > str[i]) dp[0] = str[i];if(dp[j] < str[i]){dp[j+1] = str[i];if(j == ans - 1) ans++;break;}}}return ans;}int main(){int T;string str;cin>>T;while(T--){cin>>str;cout<<length(str)<<endl;}return 0;}


    单调递增最长子序列应用:


     拦截导弹:

     原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=79

     题意:就是给一个序列然后求它的单调递减最长子序列的长度。

     思路:按照最长单调递增子序列的思路,只要把递增的比较改为递减的比较就好,length1 函数为上面解法一的修改解法, length2 为上面解法二的修改解法,而且运用二分查找存放位置。

     代码:

/*   *  http://acm.nyist.net/JudgeOnline/problem.php?pid=79 * * 求最长单调递减子序列长度 */#include<iostream>#include<cstring>#include <cstdio>using namespace std;#define MAXN 20 + 1#define max(a,b) (a)>(b)?(a):(b)  int num[MAXN];int dp[MAXN];//直接 dp ,O(n^2)int length1(int *num,int n,int *dp){int ans = 1;memset(dp,0,sizeof(dp));for(int i = 0;i < n;++i){dp[i] = 1;for(int j = 0;j < i;++j){if(num[j] > num[i]){dp[i] = max(dp[i],dp[j] + 1);ans = max(ans,dp[i]);}}}return ans;}//求最长单调递减子序列长度,运用二分int length2(int *num, int n, int *dp){int ans = 1;;memset(dp,0,sizeof(dp));dp[0] = num[0]; //初始化for(int i = 0;i < n;++i){//二分查找 num[i] 应放的位置int st = 0,ed = ans - 1;while(st <= ed){int mid = (st + ed)/2;if(dp[mid] == num[i]) break;if(dp[mid] > num[i]){st = mid + 1;}else{ed = mid - 1;}}// st > ed 的时候才要将 num[i] 放进去,用纸演算一下,要放在 dp[st] 的位置if(st > ed){dp[st] = num[i];if(st == ans) ans++;}}return ans;}int main(){int N,M;scanf("%d",&N);while(N--){scanf("%d",&M);for(int i = 0;i < M;++i)scanf("%d",&num[i]);cout<<length1(num, M, dp)<<endl;}return 0;}

    合唱队形:

    原题: http://www.rqnoj.cn/problem/26

    题意:给 n 个整数,求一个子序列,有一个中间数 i, 使得 i 的左边递增,右边递减,要这个子序列最长。输出 n - length, length 为该子序列的长度。

    思路:这还是单调递增最长子序列的问题,只是多了一步,要一个左边递增右边递减加起来最长的序列,数据较小,所以我们可以枚举。对队列里的每个数 i ,计算以它为中心的最长递增和最长递减,长度加起来和答案比较,更新 ans. 复杂度为 O(n^3) 。

    代码:

/*  * http://www.rqnoj.cn/problem/26*/#include <iostream>#include <cstring>#include <cstdio>#include <string>using namespace std;#define maxn 103int n,p[maxn],dp[maxn];int ans;int solve(){for(int i = 1;i <= n;++i){for(int j = 1;j <= n;++j){dp[j] = 1;for(int k = 1;k < j;++k){if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);}}    int l = dp[i];for(int j = n;j >= i;--j){dp[j] = 1;for(int k = n;k > j;--k){if(p[k] < p[j]) dp[j] = max(dp[j],dp[k]+1);}}int r = dp[i];ans = max(ans,l+r-1);}return n-ans;}int main(){while(scanf("%d",&n)!=EOF){for(int i = 1;i <= n;++i)scanf("%d",&p[i]);printf("%d\n",solve());}return 0;}

    子串和

     这是线性 dp 的另一种类型。给出 n 个整数,求一个连续的子串,使得这个子串的和在所有子串中最大。

     原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=44

     思路:这道题的空间复杂度可以降到 O(1),不需要把所有的数存起来,只要一边输入一边判断就好。我们用 last 表示到前一位的最大子串和,初始化是 0, 那么以某个数结尾的最大子串和的最坏情况就是它本身,如果 last >0,到当前数的最大子串和肯定是当前数加上 last,然后动态更新 ans 就好。

    代码:

/* * http://acm.nyist.net/JudgeOnline/problem.php?pid=44 */#include <iostream>#include <cstdio>using namespace std;#define INF -99999999#define max(a,b) (a)>(b)?(a):(b)int main(){int T,n,last,cur;scanf("%d",&T);while(T--){int sum = INF;last = 0;scanf("%d",&n);for(int i = 0;i < n;++i){scanf("%d",&cur);//这是关键一步,如果前面的子串和 >0,那么这一步的最大子串和为这一个数加上前一步的最大子串和if(last > 0) cur += last;sum = max(sum,cur);last = cur;}printf("%d\n",sum);}return 0;}

    01串

    原题:http://acm.nyist.net/JudgeOnline/problem.php?pid=252

    题意:长度为 n 的所有 0 1 串中,不含 11 的有多少个。

    思路:dp[i] 表示长度为 i 的 0 1串不含 11 的有多少个。初始条件是 dp[1] = 2   dp[3] = 3  

                算dp[i] 的时候,如果 i 位为 0 ,那么前 i-1 位任取,所以 dp[i] += dp[i-1] 

                如果 i 位为 1,那么 i-1 位只能为 0,前 i-2 位任取,所以 dp[i] += dp[i-2]

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

     代码:

/* * http://acm.nyist.net/JudgeOnline/problem.php?pid=252dp[i]表示第i为不含11的01串个数第i位为0,则前 i-1 位可任取第i位为1,则 i-1 位必为0,前 i-2 位任取所以 dp[i] = dp[i-1] + dp[i-2];*/#include<iostream>#include<cmath>using namespace std;#define MAXN 42int main(){int dp[MAXN],n;dp[2] = 3;dp[3] = 5;for(int i = 4;i <= 40;++i)dp[i] = dp[i-1] + dp[i-2];cin>>n;while(n--){int k;cin>>k;cout<<dp[k]<<endl;}return 0;}


    Alphacode

    原题:http://soj.me/1001

    题意:使用一种加密方法,把 A-Z 映射为 1 - 26,那么根据一个加密后的序列,可能有多种解密方法,比如给 124,你可以理解成 1、2、4,也可以看成 12、4,或者1 、24,所以要求求出共有多少中可能。

    思路:用 dp[i] 表示到第 i 位有多少种解密方法,则可以看到 dp[i] 和 dp[i-1] dp[i-2] 的关系。如果第 i 个数不是 0 ,那么这个数可以作为单独一个字母进行解码,前面 i-1 位任取,所以 dp[i] += dp[i-1] 。如果第 i-1 位是 1 或者 i-1 位是2,i 位是小于 7 的数,那么它们可以翻译为一个字母解码,前 i-2 位任取,所以 dp[i] += dp[i-2]。

   代码:

/* *http://soj.me/1001  */#include <iostream>#include <string>#include <cstring>using namespace std;int main(){string str;int ans[10000];int n;while(cin>>str&&str!="0"){memset(ans,0,sizeof(ans));ans[0] = 1;ans[1] = 1;n = str.size();for(int i=1;i<n;++i){if(str[i]!='0') ans[i+1] += ans[i];if(str[i-1]=='1'||(str[i-1]=='2'&&str[i]<'7')) ans[i+1] += ans[i-1];}cout<<ans[n]<<endl;}return 0;}

    


0 0
原创粉丝点击