—— “Hala, Abandon!”~
-------------------------------------------------------------
◊线性DP:
经典DP原型系列:
°NOIP 1999 拦截导弹 (最长下降子序列入门)
思路:比较简单,第一问最长不上升子序列;第二问贪心即可(有人证明第二问等价于求最长上升子序列。。。)。
状态转移方程:最长不上升子序列:s[i] = max(s[i], s[j]+1) (j = 0...i-1, 且a[j] >= a[i])
1 #include <cstdlib> 2 #include <algorithm> 3 #include <cstdio> 4 using namespace std; 5 6 int a[1010]; 7 int n; 8 int work1(int n) 9 { 10 int s[1010]; 11 int res = 0; 12 for (int i = 0; i < 1010; i ++) 13 s[i] = 1; 14 for (int i = 0; i < n; i ++) 15 { 16 for (int j = 0; j < i; j ++) 17 if (a[i] <= a[j]) 18 s[i] = max(s[i],s[j] + 1); 19 if (s[i] > res) res = s[i]; 20 } 21 return res; 22 } 23 int work2(int n) 24 { 25 int res = 0; 26 int vis[1010] = {0}; 27 for (int i = 0; i < n; i ++) 28 if (!vis[i]) 29 { 30 res ++; 31 vis[i] = 1; 32 int tmp = a[i]; 33 for (int j = i + 1; j < n; j ++) 34 if (a[j] <= tmp && vis[j] == 0) 35 { 36 vis[j] = 1; 37 tmp = a[j]; 38 } 39 } 40 return res; 41 } 42 int main() 43 { 44 scanf("%d",&n); 45 for (int i = 0; i < n; i ++) 46 scanf("%d",&a[i]); 47 printf("%d%d\n",work1(n),work2(n)); 48 return 0; 49 }
°NOIP 2004 合唱队形 (最长下降子序列)
思路:分别把每个点看成中间最高的人,两边的人数就是求一下从左端到该点的最长上升子序列、从右端到该点的最长下降子序列加起来。然后判断那个点做中间点时需要去除的人数最少就行了。
ºNOI 1995 石子合并 ★(石子合并原形,区间划分DP)
思路:典型的区间划分DP。f[i,j]表示区间[i..j]的石堆合并成一个的得分。因为题目中说,只能归并相邻的两堆石子。所以,f[i,j]这一系列石子必然由f[i,k]和f[k+1,j](i<=k<j)这两堆石子归并而来。只要在所有的f[i,k]+f[k+1,j]中取个最小值(就是原来此次没归并前的最小值),加上自己本身所有石子的和(因为归并一次的代价是所有石子的总和),就是我们要求的f[i,j]。
其次是要注意圆形问题转线形的方法:将石子序列倍长,答案在s[i][i + n - 1] (i = 0 .. n - 1)中找。
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 int n; 6 int a[205],s[205][205],t[205][205]; 7 int num[205][205]; 8 int calnum() 9 { 10 for (int i = 0; i < 2 * n - 1 ; i ++) 11 num[i][i] = a[i]; 12 for (int p = 1; p < n; p ++) 13 for (int i = 0; i < 2 * n - 1; i ++) 14 { 15 if (i + p < 2 * n - 1) 16 { 17 int j = i + p; 18 num[i][j] = num[i][i] + num[i + 1][j]; 19 } 20 } 21 } 22 int calmin() 23 { 24 for (int i = 0; i < 205; i ++) 25 for (int j = 0; j < 205; j ++) 26 s[i][j] = 1e8; 27 for (int i = 0; i < 2 * n - 1 ; i ++) 28 s[i][i] = 0; 29 for (int k = 1; k < n; k ++) 30 for (int i = 0; i < 2 * n - 1; i ++) 31 { 32 if (i + k < 2 * n - 1) 33 { 34 int j = i + k; 35 for (int p = i; p < j; p ++) 36 s[i][j] = min(s[i][j], s[i][p] + s[p + 1][j] + num[i][p] + num[p + 1][j]); 37 } 38 } 39 int res = 1e8; 40 for (int i = 0; i < n; i ++) 41 if (s[i][i + n - 1] < res) 42 res = s[i][i + n - 1]; 43 return res; 44 } 45 int calmax() 46 { 47 for (int p = 1; p < n; p ++) 48 for (int i = 0; i < 2 * n - 1; i ++) 49 { 50 if (i + p < 2 * n - 1) 51 { 52 int j = i + p; 53 for (int k = i; k < j; k ++) 54 t[i][j] = max(t[i][j], t[i][k] + t[k + 1][j] + num[i][k] + num[k + 1][j]); 55 } 56 } 57 int res = 0; 58 for (int i = 0; i < n; i ++) 59 if (t[i][i + n - 1] > res) 60 res = t[i][i + n - 1]; 61 return res; 62 } 63 int main() 64 { 65 scanf("%d",&n); 66 for (int i = 0; i < n; i ++) 67 { 68 scanf("%d",&a[i]); 69 a[i + n] = a[i]; 70 } 71 calnum(); 72 printf("%d\n%d\n",calmin(),calmax()); 73 return 0; 74 }
ºNOIP 2006 能量项链 (石子合并)
思路:之前做过一次,有点儿压力。。。然后去做石子合并。会了石子合并了这题就出来了,1Y。思路一样的。只不过这里一个石子有左右值。好久没1Y了,好高兴^ ^~
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 6 7 8 int n; 9 int l[210][210],r[210][210],s[210][210]; 10 int ff() 11 { 12 for (int p = 1; p < n; p ++) 13 for (int i = 0; i < 2 * n - 1; i ++) 14 if (i + p < 2 * n - 1) 15 { 16 int j = i + p; 17 for (int k = i; k < j; k ++) 18 { 19 s[i][j] = max(s[i][j], s[i][k] + s[k+1][j] + l[i][k] * r[i][k] * r[k+1][j]); 20 l[i][j] = l[i][k]; 21 r[i][j] = r[k+1][j]; 22 } 23 } 24 int res = 0; 25 for (int i = 0; i < n; i ++) 26 if (s[i][i + n - 1] > res) 27 res = s[i][i + n - 1]; 28 return res; 29 } 30 int main() 31 { 32 scanf("%d",&n); 33 for (int i = 0; i < n; i ++) 34 { 35 int q; 36 scanf("%d",&q); 37 l[i][i] = q; 38 if (i != 0) 39 r[i-1][i-1] = q; 40 if (i == n - 1) 41 r[i][i] = l[0][0]; 42 } 43 for (int i = 0; i < n; i ++) 44 { 45 l[i+n][i+n] = l[i][i]; 46 r[i+n][i+n] = r[i][i]; 47 } 48 printf("%d\n",ff()); 49 return 0; 50 }
ºNOIP 2000 乘积最大 (石子合并)
思路:这题我是按石子合并的区间DP思路做的,f[i][j][kk]表示区间[i..j]的数被kk个乘号划分成k+1个数的乘积。然后f[i][j][kk] = max{num[i][w] * f[w+1][j][kk-1]}。但是我们可以发现这个状态转移方程中j一直没变对不对?所以我们其实可以降低一维:令f[i][k]表示把前i个数用k个乘号划分成k+1个数的乘积,则f[i][k] = max{num[i][w] * f[w+1][k-1]}。
PS:这题那年普及组和提高组都有,不过普及组k<=6,数据范围int;提高组k<=30,数据范围long long。RQNOJ上是提高组的。
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 using namespace std; 5 6 int n,k; 7 long long a[50]; 8 long long f[50][50][40]; 9 long long num[50][50]; 10 void calnum() 11 { 12 for (int p = 0; p < n; p ++) 13 for (int i = 0; i < n; i ++) 14 if (i + p < n) 15 { 16 int j = i + p; 17 if (j == i) num[i][j] = a[i]; 18 else num[i][j] = num[i][j-1] * 10 + a[j]; 19 } 20 } 21 long long ff() 22 { 23 calnum(); 24 for (int kk = 0; kk <= k; kk ++) 25 for (int p = 0; p < n; p ++) 26 for (int i = 0; i < n; i ++) 27 if (i + p < n) 28 { 29 int j = i + p; 30 if (j - i < kk) continue; 31 if (kk == 0) f[i][j][kk] = num[i][j]; 32 else 33 for (int w = i; w < j; w ++) 34 f[i][j][kk] = max(f[i][j][kk], num[i][w] * f[w+1][j][kk-1]); 35 } 36 // printf("%7s%7s%7s%14s\n","i","j","kk","f"); 37 // for (int i = 0; i < n; i ++) 38 // for (int j = i; j < n; j ++) 39 // for (int kk = 0; kk <= k; kk ++) 40 // printf("%7d%7d%7d%14d\n",i,j,kk,f[i][j][kk]); 41 return f[0][n-1][k]; 42 } 43 int main() 44 { 45 scanf("%d %d%*c",&n,&k); 46 char s[50]; 47 gets(s); 48 for (int i = 0; i < n; i ++) 49 a[i] = s[i] - '0'; 50 printf("%I64d\n",ff()); 51 return 0; 52 }
ºNOIP 2001 统计单词个数 (石子合并)
思路:变相的乘积最大呐。。。就是区间num计算的方法换一下而已嘛。。。设f[i][j]为从第i个开始一直到尾划分成j+1段的单词数。
但是还是暴露除了几个值得关注的问题:区间划分DP中一定要注意判断当前区间可不可以划分,即判断[i..j]区间的大小是否大于划分数k。这个问题其实在上一道乘积最大的题目中也暴露出来了,只不过因为那里是乘的关系所以f[][]为0不会影响结果,但加可就得严格限制了。。。比如如果是aaaaaaaaaaaaaaaaaaaa要划分成4段,有一个子串aaaaa。标准答案是13,即分成[0..16][17][18][19]。不加那个if (snum - w - 1 > c - 1)判断算出来是15,他的15是由f[0][k] = num[0][18] + f[19][k - 1]得出的,但我们发现[19]根本不能划分成k段对不?
题目中还说“每份中包含的单词可以部分重叠。当选用一个单词之后,其第一个字母不能再用。例如字符串this中可包含this和is,选用this之后就不能包含th”,这个其实很好限制,我们在计算某个区间内有多少个子串时暴力枚举,当我们对区间[i,j]的串的第一个开头配对某个子串成功后直接跳出不管后面的子串而去配对下一个开头就可以了。。。(说的不是很清楚,还是看代码领悟吧。。。)
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 using namespace std; 5 6 int p,k,o; 7 int snum; 8 char a[205]; 9 char q[7][205]; 10 int num[205][205]; 11 int f[205][50]; 12 13 void calnum() 14 { 15 for (int i = 0; i < snum; i ++) 16 for (int j = i; j < snum; j ++) 17 { 18 for (int m = i; m <= j; m ++) 19 for (int w = 0; w < o; w ++) 20 { 21 int l; 22 for (l = 0; l < strlen(q[w]); l ++) 23 if (m + l > j || q[w][l] != a[m + l]) 24 break; 25 if (l == strlen(q[w])) 26 { 27 num[i][j] ++; 28 break; 29 } 30 } 31 } 32 33 // for (int i = 0; i < snum; i ++) 34 // for (int j = i; j < snum; j ++) 35 // printf("i = %d, j = %d, num = %d\n",i,j,num[i][j]); 36 } 37 int ff() 38 { 39 calnum(); 40 for (int i = 0; i < snum; i ++) 41 f[i][0] = num[i][snum - 1]; 42 for (int c = 1; c <= k; c ++) 43 for (int i = snum - 1; i >= 0; i --) 44 for (int w = i; w < snum - 1; w ++) 45 if (snum - w - 1 > c - 1) 46 f[i][c] = max(f[i][c], num[i][w] + f[w+1][c-1]); 47 return f[0][k]; 48 } 49 50 int main() 51 { 52 char s[25]; 53 scanf("%d%d",&p,&k); 54 k --; 55 while(p --) 56 { 57 scanf("%s",s); 58 for (int i = 0; i < 20; i ++) 59 a[snum ++] = s[i]; 60 } 61 62 scanf("%d",&o); 63 for (int i = 0; i < o; i ++) 64 scanf("%s",q[i]); 65 printf("%d\n",ff()); 66 return 0; 67 }
°NOIP 2000 方格取数 ★(方格取数原形,多线程DP)
一取方格数:f[i][j] = max(f[i-1][j], f[i][j-1])。二取方格数当然也有类似的O(n^4)的思路。
但有一个更好的思路:可以发现在走到第n步时,能走到的格子是固定的。进一步发现,这些格子的横纵坐标加起来都是n+1(左上角(1,1))。这样我们就不必同时记录两个坐标了,只要知道其中一个t,另一个就可以通过n+1-t算出来。这个方法在一取方格(只走一条路)时效果并不明显,这里可以看到已经减少了一维,且越是高维优点越明显。
用f[x,i,j]表示走到第x步时,第1条路线走到横坐标为i的地方,第2条路线走到了横坐标为j的地方。这样,我们只要枚举x,i,j,就能递推出来了。
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 int n; 6 int f[31][11][11]; 7 int a[11][11]; 8 int main() 9 { 10 scanf("%d",&n); 11 int x,y,c; 12 while(scanf("%d%d%d",&x,&y,&c)) 13 { 14 if (x == 0 && y == 0 && c == 0) 15 break; 16 a[x][y] = c; 17 } 18 for (int k = 1; k < n + n; k ++) 19 for (int i = 1; i <= min(n,k); i ++) 20 for (int j = 1; j <= min(n,k); j ++) 21 { 22 f[k][i][j] = max(f[k-1][i][j], f[k-1][i-1][j]); 23 f[k][i][j] = max(f[k][i][j], f[k-1][i][j-1]); 24 f[k][i][j] = max(f[k][i][j], f[k-1][i-1][j-1]); 25 if (i == j) 26 f[k][i][j] += a[i][k+1-i]; 27 else f[k][i][j] += a[i][k+1-i] + a[j][k+1-j]; 28 } 29 30 printf("%d\n",f[n+n-1][n][n]); 31 return 0; 32 }
vijos上还有三取方格数(Vijos P1143)。N取方格数这种问题可以用费用流建模做。
-------------------------------------------------------------------------------
ºNOIP 2008 传纸条 (方格取数)
思路:方格取数啊~只不过限制每个格子只能走一次,所以当两个坐标一样时直接f[][][] = 0就行了~(当然最后一步需要特殊处理一下)
ºNOIP 2005 过河 (路径压缩)
思路:DP+路径压缩。可以证明当两石块之间的距离大于它的时候,那么大于它的部分的每一个点都可以通过这两个数的某一种组合跳到,所以当两个石块间的距离大于这个最小公倍数,那么就把它们的距离缩小到这个最小公倍数.路径压缩后,就可以DP求出最小踩石子个数。设f[i]表示到达第i个位置最小踩多少个石子.则f[i]=min(f[i-j]+d[i])(1<=i<=l+t)(s<=j<=t),d[i]表示第i个位置是否有石子.最后的答案就是在l to l+t 之间找最小。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; int a[105],f[20000],d[20000]; int main(){ int l, m, s, t; memset(f,0,sizeof(f)); memset(d,0,sizeof(d)); cin >> l; cin >> s >> t >> m; for (int i = 1; i <= m; i ++){ cin >> a[i]; } int res = 0; if (s == t){ for (int i = 1; i <= m; i ++){ if (a[i] % s == 0){ res ++; } } cout<<res<<endl; } else{ sort(a+1, a+m+1); a[0] = 0; a[m+1] = l; for (int i = 0; i <= m; i ++){ if (a[i+1] - a[i] > 90){ a[i+1] = a[i] + (a[i+1] - a[i]) % 90; } } l = a[m+1]; for (int i = 1; i <= m; i ++){ d[a[i]] = 1; } for (int i = 1; i <= l + t; i ++){ f[i] = 0xFFFFFFF; } for (int i = 1; i <= l + t; i ++){ for (int j = s; j <= t; j ++){ if (i >= j){ f[i] = min(f[i], f[i-j] + d[i]); } } } int minn = 0xFFFFFFF; for (int i = l; i <= l + t; i ++){ if (f[i] < minn){ minn = f[i]; } } cout<<minn<<endl; } }
ºPOJ 2479 Maximum sum ★(POJ 2593 最大连续子段和)
题目大意:给定一个数列,找到两个不相交的子序列,使他们的和最大。
思路:因为求两个不相交的子序列,所以就是找到一个i,使得他左区间最大连续子段和+右区间最大连续子段和最大。考虑求一边的就可以了,即经典的一维最大连续子段和问题。dp[i]表示[0..i]区间的最大连续子段和,b[i]表示以i结尾的最大连续子段和。
状态转移方程:b[i] = max{b[i-1] + a[i] , a[i]}, dp[i] = max{b[j] (j = 0..i) }
时间复杂度:O(N)DP预处理+O(N)查询
#include <cstdio> #include <cstdlib> #include <climits> using namespace std; const int N = 50010; int dp1[N],dp2[N],b1[N],b2[N],a[N]; void go_dp(int n){ //left b1[0] = a[0]; dp1[0] = b1[0]; int max = dp1[0]; for (int i = 1; i < n; i ++){ if (b1[i-1] > 0){ b1[i] = b1[i-1] + a[i]; } else{ b1[i] = a[i]; } if (b1[i] > max){ max = b1[i]; } dp1[i] = max; } //right b2[n-1] = a[n-1]; dp2[n-1] = b2[n-1]; max = dp2[n-1]; for (int i = n - 2; i >= 0; i --){ if (b2[i+1] > 0){ b2[i] = b2[i+1] + a[i]; } else{ b2[i] = a[i]; } if (b2[i] > max){ max = b2[i]; } dp2[i] = max; } } int ff(int n){ int max = INT_MIN; for (int i = 0; i < n - 1; i ++){ if (dp1[i] + dp2[i+1] > max){ max = dp1[i] + dp2[i+1]; } } return max; } int main(){ int t,n; scanf("%d",&t); while(t --){ scanf("%d",&n); for (int i = 0; i < n; i ++){ scanf("%d",&a[i]); } go_dp(n); printf("%d\n",ff(n)); } //system("pause"); return 0; }
ºPOJ 1050 To the Max ★(HDU 1081 最大子矩阵和)
题目大意:最大子矩阵和。
思路:如果不是方阵而是一维数组,显然是最大连续子段和。如果已经确定这个矩形应该包含哪些行,而还不知道该包含哪些列,那么可以将每一列的各行元素相加,从而将矩阵转换为一维数组的最大连续子段和。因此,只要我们枚举矩阵应该从哪一行到哪一行,就可以将问题用最大连续子段和策略来求解了,O(N3)。
#include <cstdio> #include <cstring> #include <climits> #include <algorithm> using namespace std; int a[120][120]; int b[120],dp[120]; int main(){ int maxn; int n; while(scanf("%d",&n) == 1){ maxn = INT_MIN; for (int i = 0; i < n; i ++){ for (int j = 0; j < n; j ++){ scanf("%d",&a[i][j]); maxn = max(maxn, a[i][j]); } } for (int i = 0; i < n; i ++){ memset(b,0,sizeof(b)); for (int j = i; j < n; j ++){ dp[0] = 0; for (int k = 0; k < n; k ++){ b[k] += a[j][k]; dp[k+1] = (dp[k] > 0 ? dp[k] : 0) + b[k]; maxn = max(maxn, dp[k+1]); } } } printf("%d\n",maxn); } return 0; }
ºPOJ 3356 AGTC (字符串编辑距离(Levenshtein distance))
题目大意:给定字符串A和B。求A串转到B串所需的最少编辑操作次数。编辑操作包括:删除、添加一个字符、替换成任意字符。
思路:dp[i][j],代表字符串1的前i子串和字符串2的前j子串的距离。初始化dp[0][0] = 0,dp[i][0] = i,dp[0][j] = j;
dp[i][j] = min { dp[i][j-1] + 1 //添加(在i位置后面添加一个字符)
dp[i-1][j] + 1 //删除(删除第i个字符)
dp[i-1][j-1] + (A[i] == B[i]?0:1) //替换 }
#include <iostream> #include <cstdio> #include <cstdlib> #include <cmath> #include <vector> #include <stack> #include <queue> #include <map> #include <algorithm> #include <string> #include <cstring> #include <climits> #define MID(x,y) ((x+y)>>1) using namespace std; typedef long long LL; //max long long == 9223372036854775807LL int f[1010][1010]; char s1[1010],s2[1010]; int main(){ int l1,l2; while(cin>>l1){ cin>>s1; cin>>l2; cin>>s2; for (int i = 0; i < 1010; i ++){ for (int j = 0; j < 1010; j ++){ f[i][j] = INT_MAX; } } for (int i = 0; i <= l1; i ++){ f[i][0] = i; } for (int i = 0; i <= l2; i ++){ f[0][i] = i; } for (int i = 1; i <= l1; i ++){ for (int j = 1; j <= l2; j ++){ f[i][j] = min(f[i-1][j-1] + (s1[i-1]==s2[j-1]?0:1), f[i][j-1] + 1); f[i][j] = min(f[i][j], f[i-1][j] + 1); } } cout<<f[l1][l2]<<endl; } return 0; }
ºPOJ 1159 Palindrome ★(构造回文串 && 最长公共子串)
题目大意:给定一个字符串,问最少需要添加多少个字符(任意位置)才能构成一个回文串。
思路:答案就是:字符串长度 - 字符串和其反串的最长公共子串长度。 在求最长公共子串时,5000*5000的数组会爆内存。这里因为f[i][]只与f[i-1][]有关系,所以用一个f[2][]的滚动数组就可以完美解决了。
#include <cstdio> #include <cstring> #include <iostream> #include <algorithm> using namespace std; int f[2][5005]; char s1[5005],s2[5005]; int main(){ int n; scanf("%d",&n); scanf("%s",s1); reverse_copy(s1,s1+n,s2); //反向复制 memset(f,0,sizeof(f)); for (int i = 1; i <= n; i ++){ for (int j = 1; j <= n; j ++){ if (s1[i-1] == s2[j-1]){ f[i%2][j] = f[(i-1)%2][j-1] + 1; } else{ f[i%2][j] = max(f[(i-1)%2][j] , f[i%2][j-1]); } } } printf("%d\n",n - f[n%2][n]); return 0; }
其他DP题目:
ºCodeForces Round #150 div.1 A The Brand New Function ★(统计所有不同的结果) (一道好题,学会了DP和set的新姿势~)
题目大意:给定一个数列{a1,a2,...,an},定义f[l,r] = al | al+1 | al+2 | ... | ar 。统计所有f[l,r]的不同结果。
思路: ①转载自:http://hi.baidu.com/best_of_me/item/4a6f57053f9b56284ac4a3b9
这道题是一道DP题,因为之前没坐过这类的DP所以比赛时找不到Dp状态转移的“界”,这里暂且让我叫它 接力棒DP 吧。 题意:给n个数,任意连续[l.r]区间内的数连续 ‘|’ 就是 ’或‘,求最后得到多少个不同的结果。 简单转移思路: set<int> ans; for(i,1,n){ dp[i]=a[i]; ans.insert(dp[i]); for(j,i+1,n){ dp[j]=dp[j-1]|a[j]; ans.insert(dp[j]); } } 思路跟一般区间递推一样,但是n最大值为10^5,2s O(n^2)肯定超时。可能大家都注意到了'|'运算的的特殊性质,如果a[i]与a[j]的某一位都为1,那么 或运算 后这一位值肯定不变。 假如有 a1=31 , a2=1 , a3=2 , a4=4 ,a5=8 , a6=16 我们把这6个数的”竖着放“: 1 0 0 0 0 1 1 0 0 0 1 0 1 0 0 1 0 0 1 0 1 0 0 0 1 1 0 0 0 0 a1 a2 a3 a4 a5 a6 那么把竖着的5个位看做5个跑道 从下到上编号为1-5 a1到a2时 1号 ”交接棒“ 即a1的最低位对后面数的影响可以由a2来完成 后面依次类推, 最后到a6时,a1所有跑道都交接棒完成,a1对后面数的影响就”不存在“了。 回到题中,ai的最大值为10^6,2进制2^20就能表示了,20个跑道。 那么增加一个变量 bar 保存a[i]跑道的状态,每次往后推时更新bar的信息 当bar==0时就不在往后继续递推了。 set<int> ans; for(i,1,n){ dp[i]=a[i]; int bar=a[i]; ans.insert(dp[i]); for(j,i+1,n){ bar -= (bar&a[j]); if(bar==0) break; dp[j]=dp[j-1]|a[j]; ans.insert(dp[j]); } } cout<<ans.size()<<endl;
#include <iostream> #include <set> using namespace std; set <int> ans; int a[100010]; int dp[100010]; int main(){ int n; cin>>n; for (int i = 0; i < n; i ++){ cin>>a[i]; } for (int i = 0; i < n; i ++){ int bar = a[i]; dp[i] = a[i]; ans.insert(a[i]); for (int j = i + 1; j < n; j ++){ bar -= bar & a[j]; if (bar == 0) break; dp[j] = dp[j-1] | a[j]; ans.insert(dp[j]); } } cout<<ans.size()<<endl; return 0; }
②设dp[i][j]为以i为右区间,区间长度为j的f[],即dp[i][j] = f[i - j, i]。那么有方程dp[i][] = dp[i-1][] | a[i].但时间复杂度会达到O(n^2),无法承受。这里我们巧妙的用set来自动把前面的状态去重来减少不必要的操作,即设set <int> dp[i]为以i为右区间的所有f[*, i]。set去重的强大啊!~直接帮助我们忽略分析或运算性质的过程了。
#include <iostream> #include <set> using namespace std; const int N = 100010; set <int> dp[N],ans; int a[N]; int main(){ int n; cin>>n; for (int i = 0; i < n; i ++){ cin>>a[i]; dp[i].clear(); } ans.clear(); for (int i = 0; i < n; i ++){ dp[i].insert(a[i]); ans.insert(a[i]); if (i != 0){ set <int> ::iterator it; for (it = dp[i-1].begin(); it != dp[i-1].end(); it ++){ dp[i].insert(*it | a[i]); } for (it = dp[i].begin(); it != dp[i].end(); it ++){ ans.insert(*it); } } } cout<<ans.size()<<endl; return 0; }
ºPOJ 1080 Human Gene Functions
题目大意:给定两个串和一个串对应位置匹配的分数表,两个串如果长度不一样要添加‘-’使长度相等,求最后得分:所有匹配情况中的最高分数。
思路:看到好多解题报告说像LCS,但我觉得更像Levenshtein distance。dp[i][j]表示i字符串匹配到j字符串的情况。(要注意初始化f[i][0]和f[0][j])
状态转移方程:dp[i][j] = max{ dp[i][j-1] + t[‘-’][s2[j]] //A取‘-’,B取串
dp[i-1][j] + t[s1[i]]['-'] //A取串,B取'-'
dp[i-1][j-1] + t[s1[i]][s2[j]] //A取串,B取串 }
#include <iostream> #include <cstdio> #include <cstdlib> #include <cmath> #include <vector> #include <climits> #include <stack> #include <queue> #include <map> #include <algorithm> #include <string> #include <cstring> #define MID(x,y) ((x+y)>>1) using namespace std; typedef long long LL; //max long long == 9223372036854775807LL char s1[105],s2[105],s3[105]; int f[105][105]; int t['T'+1]['T'+1]; void initial(){ t['A']['A'] = 5; t['C']['C']=5; t['G']['G']=5; t['T']['T']=5; t['-']['-']=INT_MIN; t['A']['C']=t['C']['A']=-1; t['A']['G']=t['G']['A']=-2; t['A']['T']=t['T']['A']=-1; t['A']['-']=t['-']['A']=-3; t['C']['G']=t['G']['C']=-3; t['C']['T']=t['T']['C']=-2; t['C']['-']=t['-']['C']=-4; t['G']['T']=t['T']['G']=-2; t['G']['-']=t['-']['G']=-2; t['T']['-']=t['-']['T']=-1; return; } int main(){ int n; cin>>n; while(n--){ initial(); for (int i = 0; i < 105; i ++){ for (int j = 0; j < 105; j ++){ f[i][j] = INT_MIN; } } int l1,l2; cin>>l1>>s1; cin>>l2>>s2; if (l1 < l2){ strcpy(s3,s1); strcpy(s1,s2); strcpy(s2,s3); int tmp = l1; l1 = l2; l2 = tmp; } f[0][0] = 0; f[1][0] = t[s1[0]]['-']; f[0][1] = t['-'][s2[0]]; for (int i = 2; i <= l1; i ++){ f[i][0] = f[i-1][0] + t[s1[i-1]]['-']; f[0][i] = f[0][i-1] + t['-'][s2[i-1]]; } for (int i = 1; i <= l1; i ++){ for (int j = 1; j <= l2; j ++){ f[i][j] = f[i-1][j-1] + t[s1[i-1]][s2[j-1]]; f[i][j] = max(f[i][j], f[i][j-1] + t['-'][s2[j-1]]); f[i][j] = max(f[i][j], f[i-1][j] + t[s1[i-1]]['-']); } } cout<<f[l1][l2]<<endl; } return 0; }
ºPOJ 3267 The Cow Lexicon ★(BZOJ 1633)
题目大意:给定一个字符串和一个字典(一堆串),问最少需要从字符串中删除几个字符才能使得字符串全部由下面的字典串构成(不重叠)。
思路:(想了2、3天了,但做完后发现其实挺简单的= =。。。)设f[i]表示以i开始到字符串尾要合法化最少需要删除的字符数,则
状态转移方程: f[i] = min{ f[j] + merge(str[i..j], s[k]) } //如果str[i..j]可以匹配第k个字典串,merge返回[i..j]这一段儿匹配s[k]需要删除的字符数
= f[i+1] + 1 //如果str[i..j]中不可以匹配所有s[]。
开始在纠结怎么算merge,因为一个区间中对某字典串可能有好几次匹配,但仔细想后发现对同一个字符串,一定是j越小越好(至少不会更差,想一下~),所以一个字典串只用匹配一次~~然后又纠结怎么匹配——先想到了如果 LCS(str[i..j],s[k]) == strlen(s[k])就是匹配成功了嘛,那样总复杂度达到O(m2np),TLE了。然后终于想到怎么匹配了= =……就用两个指针嘛,匹配一对儿两个指针同时向后移,当第二个指针到尾了就匹配成功了,顺便算出位置啥的~~~
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; char str[400],s[650][30]; int f[400]; int merge(char *s, char *p, int po){ int l1 = po; int l2 = 0; int len1 = strlen(p); int len2 = strlen(s); while(l1 < len1 && l2 < len2){ if(p[l1] == s[l2]){ l2 ++; } l1 ++; } if (l2 < len2){ return -1; } else{ return l1 - 1; } } int main(){ int n; int l; scanf("%d%d",&n,&l); scanf("%s",str); for (int i = 0; i < n; i ++){ scanf("%s",s[i]); } memset(f,0x7f,sizeof(f)); f[l] = 0; for (int i = l - 1; i >= 0; i --){ for (int p = 0; p < n; p ++){ int minj = -1; // LCS // for (int j = i; j < l; j ++){ // for (int k = 0; k < strlen(s[p]); k ++){ // re[j+1][k+1] = max(re[j+1][k], re[j][k+1]); // if (str[j] == s[p][k]) // re[j+1][k+1] = max(re[j+1][k+1], re[j][k]+1); // } // if (re[j+1][strlen(s[p])] == strlen(s[p])){ // minj = j; // break; // } // } minj = merge(s[p], str, i); if (minj != -1){ f[i] = min(f[i], f[minj+1] + (minj - i + 1) - (int)strlen(s[p])); } else{ f[i] = min(f[i], f[i+1] + 1); } } } printf("%d\n",f[0]); return 0; }
◊树形DP:针对在树上的动态规划。做树形DP一般步骤是先将树转换为有根树,然后在树上进行深搜操作:从子节点向树根返回信息或者从树根想子节点传递信息。
ºHDU 1520 Anniversary party (POJ 2342,树形DP入门题 && 最大独立子集)
题目大意:给定一棵关系树,每个节点有个权值,子节点和父节点不能同时选,问最后能选的最大价值是多少?
思路:由于子节点与父节点不能同时选,有人可能会用贪心思想,二者选其一肯定最优。其实不然,有可能父节点和子节点都不选,而要选子孙节点。不过只要再往深点想下,就可以得出动态规划的解法。每个节点要么选要么不选,和大多数选不选动归一样,来个dp[i][2],0表示不选,1表示不选,那我们只要从叶子节点往根结点不断更新dp[i][0]和dp[i][1]就可以了。
状态转移方程: dp[i[[1] = sum(dp[j][0]) (当前选了,子节点必定不能选,最优的情况是都不选,然后累加)
dp[i][0] = sum(max(dp[i][0],dp[i][1])) (当选不选,子节点可选可不选,找大的那个状态)
#include <iostream> #include <cstdio> #include <cstring> #include <vector> using namespace std; const int N = 6005; vector <int> v[N]; int f[N][2],a[N],indegree[N]; int vis[N]; int tree_dp(int n) { if (!vis[n]){ vis[n] = 1; for (int i = 0; i < v[n].size(); i ++){ int p = v[n][i]; if (!vis[p]){ tree_dp(p); f[n][0] += max(f[p][0], f[p][1]); f[n][1] += f[p][0]; } } } f[n][1] += a[n]; return max(f[n][0],f[n][1]); } int main(){ int n; while(scanf("%d",&n)!=EOF){ for (int i = 1; i <= n; i ++){ scanf("%d",&a[i]); } for (int i = 1; i <= n; i ++){ v[i].clear(); } memset(indegree,0,sizeof(indegree)); memset(f,0,sizeof(f)); memset(vis,0,sizeof(vis)); int a,b; while(scanf("%d%d",&a,&b)){ if (a + b == 0){ break; } indegree[a] ++; v[b].push_back(a); } for (int i = 1; i <= n; i ++){ if (indegree[i] == 0){ printf("%d\n",tree_dp(i)); break; } } } }
ºHDU 2412 Party at Hali-Bula (POJ 3342,树形DP入门题 && 最大独立子集)
题目大意:给定一棵关系树,子节点和父节点不能同时选,问最后最多能选多少点?并且判断最大解是否唯一。
思路:找最多能选几个点和上面那道题是一样的,这题的难点在于怎么判断解唯一。我们再设一个flag[i][2],来表示dp[i][2]的方案是否唯一。
状态转移方程:flag的方程在代码中标注的地方
这题竟然WA了10+次。。。==||,因为我刚开始以为每个人的老板的信息已经在前面出现过了,事实上不是这样==||。。。
#include <iostream> #include <cstdio> #include <string> #include <vector> #include <cstring> #include <algorithm> #include <map> using namespace std; const int N = 210; map <string, int> m; vector <int> v[N]; int dp[N][2]; bool flag[N][2],vis[N]; void tree_dp(int n){ if (!vis[n]){ vis[n] = true; for (int i = 0; i < v[n].size(); i ++){ int p = v[n][i]; if (!vis[p]){ tree_dp(p); dp[n][0] += max(dp[p][0], dp[p][1]); //flag的状态转移方程 if (dp[p][0] == dp[p][1]){ flag[n][0] = true; } else if (dp[p][0] > dp[p][1] && flag[p][0]){ flag[n][0] = true; } else if (dp[p][1] > dp[p][0] && flag[p][1]){ flag[n][0] = true; } dp[n][1] += dp[p][0]; if (flag[p][0]) flag[n][1] = true; } } dp[n][1] ++; } } int main(){ int n; while(cin>>n){ if (n == 0){ break; } int mnum = 1; m.clear(); for (int i = 0; i < N; i ++){ v[i].clear(); } memset(vis,0,sizeof(vis)); memset(dp,0,sizeof(dp)); memset(flag,false,sizeof(flag)); string s; cin>>s; m[s] = mnum ++; for (int i = 1; i < n; i ++){ string s1,s2; cin>>s1>>s2; if (m.find(s1) == m.end()) m[s1] = mnum ++; if (m.find(s2) == m.end()) m[s2] = mnum ++; v[m[s2]].push_back(m[s1]); } tree_dp(1); if (dp[1][0] == dp[1][1]){ cout<<dp[1][1]<<" No\n"; } else{ if (dp[1][0] > dp[1][1]){ if (flag[1][0]) cout<<dp[1][0]<<" No\n"; else{ cout<<dp[1][0]<<" Yes\n"; } } else if (dp[1][0] < dp[1][1]){ if (flag[1][1]) cout<<dp[1][1]<<" No\n"; else{ cout<<dp[1][1]<<" Yes\n"; } } } } return 0; }
ºSGU 149 Computer Network ★(HDU 2196 Computer,经典树形DP)
题目大意:给一棵树,每条树边都有权值,问从每个顶点出发,经过的路径权值最大为多少?每条树边都只能走一次,n <= 10000,权值<=10^9
思路:以1为根固定树。然后两遍DFS,第一遍从子节点到根节点更新,求出每个节点在以它为根的子树上的最远距离和次远距离(并且要保证他俩不在一条支路上,这个为第二次DFS做准备),第二遍DFS从根节点到子节点更新,因为每个节点的最远路径不一定在他的子树上,所以要通过父亲的最远路经对节点进行一次更新。但有个问题就是万一父亲节点的最远路经正好在该节点子树这条路上怎么办,则此时我们选择不在这个支路上的那条父亲节点的次长路来对节点更新。
#include <cstdio> #include <cstring> #include <cstdlib> #include <vector> #include <algorithm> using namespace std; const int N = 10010; vector <pair<int,int> > v[N]; int dp1[N],dp2[N],ndp1[N],ndp2[N]; //最远路和次远路 int vis[N]; void dfs(int n,int root){ if (!vis[n]){ vis[n] = 1; for (int i = 0; i < v[n].size(); i ++){ int p = v[n][i].first; if (p == root) continue; if (!vis[p]){ dfs(p, n); //更新从子节点上来的信息,因为dp1和dp2不能在同一条支路上,所以只用对每个子节点的dp1来更新。 if (dp1[p] + v[n][i].second > dp1[n]){ dp2[n] = dp1[n]; ndp2[n] = ndp1[n]; dp1[n] = dp1[p] + v[n][i].second; ndp1[n] = p; } else{ if (dp1[p] + v[n][i].second > dp2[n]){ dp2[n] = dp1[p] + v[n][i].second; ndp2[n] = p; } } } } } } void tree_dp(int n, int root){ if (!vis[n]){ vis[n] = 1; //先通过父节点更新节点 for (int i = 0; i < v[n].size(); i ++){ if (v[n][i].first == root){ if (ndp1[root] != n){ dp2[n] = dp1[n]; ndp2[n] = ndp1[n]; dp1[n] = dp1[root] + v[n][i].second; ndp1[n] = root; } else{ if (dp2[root] + v[n][i].second > dp1[n]){ dp2[n] = dp1[n]; ndp2[n] = ndp1[n]; dp1[n] = dp2[root] + v[n][i].second; ndp1[n] = root; } else if (dp2[root] + v[n][i].second > dp2[n]){ dp2[n] = dp2[root] + v[n][i].second; ndp2[n] = root; } } } } //从节点向子节点传递信息 for (int i = 0; i < v[n].size(); i ++){ if (v[n][i].first != root){ int p = v[n][i].first; tree_dp(p, n); } } } } int main(){ int n; while(scanf("%d",&n)!=EOF){ for (int i = 0; i <= n; i ++) v[i].clear(); memset(vis,0,sizeof(vis)); memset(dp1,0,sizeof(dp1)); memset(dp2,0,sizeof(dp2)); memset(ndp1,0,sizeof(ndp1)); memset(ndp2,0,sizeof(ndp2)); for (int i = 2; i <= n; i ++){ int a,b; scanf("%d%d",&a,&b); v[i].push_back(make_pair(a,b)); v[a].push_back(make_pair(i,b)); } dfs(1,0); memset(vis,0,sizeof(vis)); tree_dp(1,0); for (int i = 1; i <= n; i ++){ printf("%d\n",dp1[i]); } } //system("pause"); return 0; }
◊插头DP:
(未完待续...)