本文为该书的笔记:刘汝佳. 算法竞赛入门经典.第2版[M]. 清华大学出版社, 2014.
动态规划的思想:从复杂的题目背景中抽象出状态表示,然后设计它们之间的转移。最长上升子序列问题(LTS)
状态: 表示以 为结尾的最长上升子序列的长度(即 必定为为子序列的最后一个)。 则状态传递函数为:
如果需要打印字典序。
样例:61 6 2 3 7 5复制代码
输出:
41 2 3 5复制代码
完整程序:
#define LOCAL#include#include #include #include //#include #include #include #include #include #include #include #include #include #include #include
最长公共子序列问题(LCS)
样例:一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 称为已知序列的最长公共子序列。
6 71 5 2 6 8 72 3 5 6 9 8 4复制代码
输出:
3复制代码
状态: 表示 , ... 和 , ... 的 LCS 。
状态转移方程:#define LOCAL#include#include #include #include #include #include #include #include #include #include #include #include #include #include
照明系统设计()
样例:
254376 266 7 8020986 679 2 50复制代码
输出:
1176复制代码
可以得到结论: A 灯泡换成 B 灯泡,要么全都换,要么全都不换。因为 如果A的一部分换成了B,电源的费用是没有变的,可能省下来的只有当B的灯泡费用小于A的时候的一部分灯泡费用,那么为什么不讲A的所有可以省的灯泡费用都省下来呢?
当 且 (即 )时更换。 即如果i可以换成j,j可以换成k,则i则需要换成k。先把灯泡按照电压从小到大排序,设 为前 种灯泡的总数量。
状态: 为前 种灯泡的最小开销 状态转移方程:其中 为将第 , ... 都换成 灯泡之后的开销, 为转换之后这部分的灯泡总费用,
完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组#define MAXN 1002using namespace std;int n;int d[MAXN];int s[MAXN];struct Lamp{ int v,k,c,l; bool operator < (const Lamp& var){ return v > n && n){ memset(d,-1,sizeof(d));// cin >> C; for(int i=1;i<=n;i++){ cin >> lamp[i].v >> lamp[i].k >> lamp[i].c >> lamp[i].l; } sort(lamp+1,lamp+n+1); s[1]=lamp[1].l; for(int i=2;i<=n;i++){ s[i]=s[i-1]+lamp[i].l; } cout << dp(n) << endl; return 0;}复制代码
划分成回文串()
状态不难想到,困难的是状态转移方程的确定。 状态: 为字符0~ 划分成的最小回文串个数。 状态转移方程:d[i]=min{d[j]+1|s[j+1~i]是回文串}复制代码
使用动态规划判断是s[i~j]是否为回文串: 状态:为bool型,表示i~j为回文串。
b[i][j]=(s[i] == s[j]) && b[i+1][j-1]复制代码
完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组#define MAXN 1002using namespace std;const int INF = 1 << 30;int C;int n, m;string s;int b[MAXN][MAXN];int d[MAXN];int bp(int i, int j){ int &ans = b[i][j]; if (ans != -1) return ans; if (i == j) return ans = 1; if (i > j) return 1; if (s[i] == s[j] && bp(i + 1, j - 1)) { return ans = 1; } else { return ans = 0; }}int dp(int i){ int &ans = d[i]; if (ans != -1) { return ans; } if (bp(0, i)) { ans = 1; } else { ans = INF; } for (int j = 0; j < i; j++) { if (bp(j + 1, i)) { ans = min(ans, dp(j) + 1); } } return ans;}int V, W;int A[101], B[101];int rem[101];int main(){#ifdef LOCAL freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);#endif // LOCAL int N; cin >> N; getline(cin, s); while (N--) { memset(b, -1, sizeof(b)); memset(d, -1, sizeof(d)); d[0] = 1; getline(cin, s); n = s.length(); cout << dp(n - 1) << endl; } return 0;}复制代码
颜色的长度()
完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组using namespace std;const int maxn = 5000 + 5;const int INF = 1 << 30;char p[maxn], q[maxn]; //p q为输入的字符序列int n, m; //输入的两个序列的长度int sp[26], ep[26], sq[26], eq[26]; //两个序列每个字母的起始和结束位置int d[maxn][maxn];int c[maxn][maxn];/**< 有多少颜色已经开始但是尚未结束 */int color(int i, int j){ int c = 0; for (int ii = 0; ii < 26; ii++) { if ((sp[ii] <= i || sq[ii] <= j) && (ep[ii] > i || eq[ii] > j)) { c++; } } return c;}/**< p序列移走了前i个,q移走了前j个,这i+j个字符的跨度最小和*/int dp(int i, int j){ int &ans = d[i][j]; if (ans != -1) { return ans; } if (!i && j) { ans = dp(i, j - 1) + c[i][j - 1]; } else if (i && !j) { ans = dp(i - 1, j) + c[i - 1][j]; } else { ans = min(dp(i - 1, j) + c[i - 1][j], dp(i, j - 1) + c[i][j - 1]); } // cout < <<" "< << " "< < > N; while (N--) { memset(d, -1, sizeof(d)); cin >> p + 1; cin >> q + 1; n = strlen(p + 1); m = strlen(q + 1); for (int i = 1; i <= n; i++) { p[i] -= 'A'; } for (int i = 1; i <= m; i++) { q[i] -= 'A'; } for (int i = 0; i < 26; i++) { sp[i] = sq[i] = INF; ep[i] = eq[i] = 0; } for (int i = 1; i <= n; i++) { sp[p[i]] = min(sp[p[i]], i); ep[p[i]] = i; } for (int i = 1; i <= m; i++) { sq[q[i]] = min(sq[q[i]], i); eq[q[i]] = i; } d[0][0] = 0; for (int i = 0; i <= n; i++) for (int j = 0; j <= m; j++) { c[i][j] = color(i, j); } cout << dp(n, m) << endl; } return 0;}复制代码
最优矩阵链乘
假设第 个矩阵 是
状态 表示:“把 , , ... , 乘起来最少需要多少次乘法”。 状态转移方程:样例:
32 3 4 5复制代码
结果:
64复制代码
完整程序:
记忆化搜索:#define LOCAL#include#include #include #include //操作字符串和数组using namespace std;const int maxn = 100 + 5;const int INF = 1 << 30;int p[maxn];int d[maxn][maxn];int n;int dp(int i, int j){ int &ans = d[i][j]; if (ans != -1) { return ans; } ans = INF; for (int k = i; k <= j; k++) { ans = min(ans, dp(i, k) + dp(k + 1, j) + p[i - 1] * p[k] * p[j]); } return ans;}int main(){#ifdef LOCAL freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);#endif // LOCAL while (cin >> n && n) { memset(d, -1, sizeof(d)); for (int i = 0; i <= n; i++) { cin >> p[i]; d[i][i] = 0; } cout << dp(1, n) << endl; } return 0;}复制代码
递推法:
#define LOCAL #include#include #include #include //操作字符串和数组 using namespace std; const int maxn = 100 + 5; const int INF = 1 << 30; int p[maxn]; int d[maxn][maxn]; int n; int main() { #ifdef LOCAL freopen("data.in", "r", stdin); freopen("data.out", "w", stdout); #endif // LOCAL while (cin >> n && n) { memset(d, -1, sizeof(d)); for (int i = 0; i <= n; i++) { cin >> p[i]; d[i][i] = 0; } for (int delta = 1; delta < n; delta++) { for (int i = 1; i + delta <= n; i++) { d[i][i + delta] = INF; for (int k = i; k <= i + delta; k++) { d[i][i + delta] = min(d[i][i + delta], d[i][k] + d[k + 1][i + delta] + p[i - 1] * p[k] * p[i + delta]); } } } cout << d[1][n] << endl; } return 0; } 复制代码
最优三角剖分
对于一个n个顶点的凸多边形,有很多种方法可以对它进行三角剖分(triangulation),即用n-3条互不相交的对角线把凸多边形分成n-2个三角形。为每个三角形规定一个权函数w(i, j,k)(如三角形的周长或3个顶点的权和),求让所有三角形权和最大的方案。
最优三角形剖分与最优矩阵链乘的不同:链乘表达式反映了决策过程,而剖分不反映决策过程,即在链乘问题里面对于某一解决策的过程是确定的,而在三角形剖分里面“第一刀”可以是这一个解里面的任何一条对角线。
如果允许随意切割,则半成品多边形的各个顶点可以在原多边形中任意选取的,很难简洁定义成状态。所以有必要把决策的顺序规范化,使得在规范的决策顺序下,任意状态都可以用区间表示。 定义 为子多边形 , , ... , 的最优值,则边 在最优解中一定对应一个三角形 ( )。 状态转移方程:原问题的解为 。
切木棍()
状态: 为切割 j$ 部分的最小费用。 状态转移方程:
完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组using namespace std;const int maxn = 50 + 3;const int INF = 1 << 30;int n;int L;int a[maxn];int d[maxn][maxn];int dp(int i,int j){ if(j==i+1){ return 0; } int& ans=d[i][j]; if(ans != -1){ return ans; } ans=INF; for(int k=i+1;k > L && L){ memset(d,-1,sizeof(d)); memset(a,0,sizeof(a)); cin >> n; for(int i=1;i<=n;i++){ cin >> a[i]; } a[0]=0; a[n+1]=L; cout <<"The minimum cutting is "<< dp(0,n+1)<< "." << endl; } return 0;}复制代码
括号序列():
定义如下正规括号序列(字符串):
- 空序列是正规括号序列。
- 如果S是正规括号序列,那么(S)和[S]也是正规括号序列。
- 如果A和B都是正规括号序列,那么AB也是正规括号序列。
例如,下面的字符串都是正规括号序列:(),[],(()),([]),()[],()[()],而如下字符串则不是正规括号序列:(,[,],)(,([()。
输入一个长度不超过100的,由“(”、“)”、“[”、“]”构成的序列,添加尽量少的括号,得到一个规则序列。如有多解,输出任意一个序列即可。
完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组#include using namespace std;const int maxn = 100 + 3;const int INF = 1 << 30;int n;string S;int d[maxn][maxn];int match(char a, char b){ int c = 0; if (a == '(' && b == ')') { c = 1; } else if (a == '[' && b == ']') { c = 1; } return c;}int dp(int i, int j){ int &ans = d[i][j]; if (ans != -1) { return ans; } if (i > j) return ans = 0; if (i == j) { return ans = 1; } ans = INF; if (match(S[i], S[j])) { ans = dp(i + 1, j - 1); } for (int k = i; k < j; k++) { ans = min(ans, dp(i, k) + dp(k + 1, j)); } return ans;}void print(int i, int j){ if (i > j) return; if (i == j) { if (S[i] == '(' || S[i] == ')') { printf("()"); } else { printf("[]"); } return; } int ans = d[i][j]; if (match(S[i], S[j]) && ans == d[i + 1][j - 1]) { printf("%c", S[i]); print(i + 1, j - 1); printf("%c", S[j]); return; } for (int k = i; k < j; k++) { if (ans == d[i][k] + d[k + 1][j]) { print(i, k); print(k + 1, j); return; } }}int main(){#ifdef LOCAL freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);#endif // LOCAL int N; cin >> N; getline(cin, S); while (N--) { memset(d, -1, sizeof(d)); getline(cin, S); getline(cin, S); int n = S.length(); dp(0, n - 1); print(0, n - 1); cout << endl; cout << endl; } return 0;}复制代码
最大面积最小的三角剖分()
这实际是一个最优三角形剖分问题。
状态:设为子多边形 , , ... , 的最优值(最大面积最小的三角形的最大面积),则边 在最优解中一定对应一个三角形 ( )。 状态转移方程:原问题的解为 。 完整程序:
#define LOCAL#include#include #include #include //操作字符串和数组#include #include using namespace std;const int maxn = 50 + 2;const int INF = 1 << 30;int n;double point[maxn][2];double d[maxn][maxn];int vis[maxn][maxn];//求三角形的面积double w(int a1, int b1, int c1){ //求三条边长 double a = sqrt(pow(point[a1][0] - point[b1][0], 2.0) + pow(point[a1][1] - point[b1][1], 2.0)); double b = sqrt(pow(point[c1][0] - point[b1][0], 2.0) + pow(point[c1][1] - point[b1][1], 2.0)); double c = sqrt(pow(point[a1][0] - point[c1][0], 2.0) + pow(point[a1][1] - point[c1][1], 2.0)); double s = (a + b + c) / 2; double A = sqrt(s * (s - a) * (s - b) * (s - c)); return A;}double dp(int i, int j){ double area; double &ans = d[i][j]; if (vis[i][j]) { return ans; } if (j == i + 2) { vis[i][j] = 1; return ans = w(i, i + 1, j); } if (j == i + 1) { vis[i][j] = 1; return ans = 0; } ans = INF * 1.0; for (int k = i + 1; k <= j - 1; k++) { area = w(i, j, k); area = max(area, dp(i, k)); area = max(area, dp(k, j)); ans = min(ans, area); } vis[i][j] = 1; return ans;}int main(){#ifdef LOCAL freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);#endif // LOCAL int N; cin >> N; while (N--) { memset(vis, 0, sizeof(vis)); cin >> n; //输入点的值 for (int i = 0; i < n; i++) { cin >> point[i][0] >> point[i][1]; } //状态(0,n-1)的最优解 cout << dp(0, n - 1) << endl; } return 0;}复制代码