本文为该书的笔记:刘汝佳. 算法竞赛入门经典.第2版[M]. 清华大学出版社, 2014.
动态规划的思想:从复杂的题目背景中抽象出状态表示,然后设计它们之间的转移。
最长上升子序列问题(LTS)
状态:
d
(
i
)
d(i)
d(i) 表示以
i
i
i 为结尾的最长上升子序列的长度(即
A
i
A_i
Ai 必定为为子序列的最后一个)。
则状态传递函数为:
d
(
i
)
=
m
a
x
{
0
,
d
(
j
)
∣
j
<
i
,
A
j
<
A
i
}
d(i)=max \left \{ 0,d(j)|j<i,A_j<A_i \right \}
d(i)=max{0,d(j)∣j<i,Aj<Ai}
如果需要打印字典序。
样例:
6
1 6 2 3 7 5
输出:
4
1 2 3 5
完整程序:
#define LOCAL
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
//#include <stdio.h>
#include <cstdlib>
#include<time.h>
#include <ctype.h>
#include <sstream>
#include <assert.h>
#include <math.h>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <stack>
#include <queue>
#define MAXN 92
using namespace std;
int A[101];
int rem[101];
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
int dmax=0,cur=0;
while(cin >> n&& n){
memset(d,0,sizeof(d));
for(int i=1;i<=n;i++){
cin >> A[i];
}
for(int i=1;i<=n;i++){
d[i]=1;
for(int j=1;j<i;j++){
if(A[j]<A[i]){
d[i]=max(d[i],d[j]+1);
}
}
}
for(int i=1;i<=n;i++){
if(d[i]>dmax){
cur=i;
dmax=d[i];
}
}
cout << dmax << endl;
rem[d[cur]] = A[cur];
while(d[cur]>1){
for(int j=1;j<cur;j++){
if(A[j]<A[cur] && d[j]+1 == d[cur]){
cur = j;
rem[d[cur]]=A[cur];
}
}
}
for(int i=1;i<=dmax;i++){
cout << rem[i] << " ";
}
return 0;
}
最长公共子序列问题(LCS)
一个数列 S {\displaystyle S} S ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则 S {\displaystyle S} S 称为已知序列的最长公共子序列。
维基百科介绍
样例:
6 7
1 5 2 6 8 7
2 3 5 6 9 8 4
输出:
3
状态:
d
(
i
,
j
)
d(i,j)
d(i,j) 表示
A
1
A_1
A1 ,
A
2
A_2
A2 …
A
i
A_i
Ai 和
B
1
B_1
B1 ,
B
2
B_2
B2 …
B
j
B_j
Bj 的 LCS 。
状态转移方程:
d ( i , j ) = { d ( i − 1 , j − 1 ) + 1 , A i = A j m a x { d ( i − 1 , j ) , d ( i , j − 1 ) } , A i ≠ A j d(i,j)= \begin{cases} d(i-1,j-1)+1 & \text{ , } A_i=A_j \\ max \left \{ d(i-1,j),d(i,j-1) \right \} & \text{ , } A_i \neq A_j \end{cases} d(i,j)={d(i−1,j−1)+1max{d(i−1,j),d(i,j−1)} , Ai=Aj , Ai̸=Aj
#define LOCAL
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
#include <cstdlib>
#include <time.h>
#include <ctype.h>
#include <sstream>
#include <assert.h>
#include <math.h>
#include <algorithm>
#include <vector>
#include <set>
#include <map>
#include <stack>
#include <queue>
#define MAXN 92
using namespace std;
int C;
//int d[10002];
int d[101][101];
int n,m;
int V,W;
int A[101],B[101];
int rem[101];
int dp(int i,int j){
if(i<=0 || j<=0){
return 0;
}
// cout << i << " " << j << endl;
int& ans=d[i][j];
cout << ans << endl;
if(ans != -1){
return ans;
}
if(A[i] == B[j]){
return ans=dp(i-1,j-1)+1;
}
return ans=max(dp(i-1,j),dp(i,j-1));
}
int main()
{
#ifdef LOCAL
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
#endif // LOCAL
int dmax=0,cur=0;
while(cin >> n >> m && n && m){
memset(d,-1,sizeof(d));
for(int i=1;i<=n;i++){
cin >> A[i];
}
for(int j=1;j<=m;j++){
cin >> B[j];
}
d[0][0]=d[0][1]=d[1][0]=0;
cout << dp(n,m);
return 0;
}
照明系统设计(UVa11400)
样例:
2
54376 266 7 80
20986 679 2 50
输出:
1176
可以得到结论: A 灯泡换成 B 灯泡,要么全都换,要么全都不换。因为
如果A的一部分换成了B,电源的费用是没有变的,可能省下来的只有当B的灯泡费用小于A的时候的一部分灯泡费用,那么为什么不讲A的所有可以省的灯泡费用都省下来呢?
当
V
i
<
V
j
V_i<V_j
Vi<Vj 且
K
i
−
(
C
j
−
C
i
)
L
i
>
0
K_i-(C_j-C_i)L_i>0
Ki−(Cj−Ci)Li>0 (即 $ \frac{ K_i}{L_i} +C_i>C_j$ )时更换。
即如果i可以换成j,j可以换成k,则i则需要换成k。
先把灯泡按照电压从小到大排序,设
s
i
s_i
si 为前
i
i
i 种灯泡的总数量。
状态:
d
[
i
]
d[i]
d[i] 为前
i
i
i 种灯泡的最小开销
状态转移方程:
d
[
i
]
=
m
i
n
{
d
[
j
]
+
(
s
[
i
]
−
s
[
j
]
)
∗
C
[
i
]
+
K
[
i
]
}
d[i]=min \left \{ d[j]+(s[i]-s[j])*C[i]+K[i] \right \}
d[i]=min{d[j]+(s[i]−s[j])∗C[i]+K[i]}
其中
(
s
[
i
]
−
s
[
j
]
)
∗
C
[
i
]
+
K
[
i
]
(s[i]-s[j])*C[i]+K[i]
(s[i]−s[j])∗C[i]+K[i] 为将第
i
+
1
i+1
i+1 ,
i
+
2
i+2
i+2 …
j
j
j都换成
j
j
j 灯泡之后的开销,
(
s
[
i
]
−
s
[
j
]
)
∗
C
[
i
]
(s[i]-s[j])*C[i]
(s[i]−s[j])∗C[i] 为转换之后这部分的灯泡总费用,
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>//操作字符串和数组
#define MAXN 1002
using namespace std;
int n;
int d[MAXN];
int s[MAXN];
struct Lamp{
int v,k,c,l;
bool operator < (const Lamp& var){
return v<var.v;
}
};
Lamp lamp[MAXN];
int dp(int i){
int& ans=d[i];
if(ans != -1){
return ans;
}
ans=s[i]*lamp[i].c+lamp[i].k;
for(int j=1;j<i;j++){
ans=min(ans,dp(j)+(s[i]-s[j])*lamp[i].c+lamp[i].k);
}
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));
// 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;
}
划分成回文串(UVa11584)
状态不难想到,困难的是状态转移方程的确定。
状态:
d
[
i
]
d[i]
d[i] 为字符0~
i
i
i划分成的最小回文串个数。
状态转移方程:
d[i]=min{d[j]+1|s[j+1~i]是回文串}
使用动态规划判断是s[i~j]是否为回文串:
状态:
b
[
i
]
[
j
]
b[i][j]
b[i][j]为bool型,表示i~j为回文串。
b[i][j]=(s[i] == s[j]) && b[i+1][j-1]
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#define MAXN 1002
using 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;
}
颜色的长度(UVa1625)
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
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 <<i<<" "<<j<< " "<<ans <<endl;
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(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;
}
最优矩阵链乘
假设第
i
i
i 个矩阵
A
i
A_i
Ai 是
p
i
−
1
×
p
i
p_{i-1} \times p_i
pi−1×pi
状态
f
(
i
,
j
)
f(i,j)
f(i,j) 表示:“把
A
i
A_i
Ai ,
A
i
+
1
A_{i+1}
Ai+1 , … ,
A
j
A_j
Aj乘起来最少需要多少次乘法”。
状态转移方程:
f
(
i
,
j
)
=
m
i
n
{
f
(
i
,
k
)
+
f
(
k
+
1
,
j
)
+
p
i
−
1
p
k
p
j
}
f(i,j)=min \left \{ f(i,k)+f(k+1,j)+p_{i-1} p_k p_j \right \}
f(i,j)=min{f(i,k)+f(k+1,j)+pi−1pkpj}
样例:
3
2 3 4 5
结果:
64
完整程序:
记忆化搜索:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
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 <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
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个顶点的权和),求让所有三角形权和最大的方案。
最优三角形剖分与最优矩阵链乘的不同:链乘表达式反映了决策过程,而剖分不反映决策过程,即在链乘问题里面对于某一解决策的过程是确定的,而在三角形剖分里面“第一刀”可以是这一个解里面的任何一条对角线。
如果允许随意切割,则半成品多边形的各个顶点可以在原多边形中任意选取的,很难简洁定义成状态。所以有必要把决策的顺序规范化,使得在规范的决策顺序下,任意状态都可以用区间表示。
定义
d
(
i
,
j
)
d(i,j)
d(i,j) 为子多边形
i
i
i ,
i
+
1
i+1
i+1 , … ,
j
j
j 的最优值,则边
i
−
j
i-j
i−j在最优解中一定对应一个三角形
i
−
j
−
k
i-j-k
i−j−k (
i
<
k
<
j
i<k<j
i<k<j )。
状态转移方程:
d
(
i
,
j
)
=
m
a
x
{
d
(
i
,
k
)
+
d
(
k
,
j
)
+
w
(
i
,
j
,
k
)
∣
i
<
k
<
j
}
d(i,j)=max \left \{ d(i,k)+d(k,j)+w(i,j,k)|i<k<j \right \}
d(i,j)=max{d(i,k)+d(k,j)+w(i,j,k)∣i<k<j}
原问题的解为
d
(
0
,
n
−
1
)
d(0,n-1)
d(0,n−1) 。
切木棍(UVa10003)
状态:
d
(
i
,
j
)
d(i,j)
d(i,j) 为切割 $i ~
j
j
j 部分的最小费用。
状态转移方程:
d
(
i
,
j
)
=
m
i
n
{
d
(
i
,
k
)
+
d
(
k
,
j
)
+
a
j
−
a
i
∣
i
<
k
<
j
}
d(i,j)=min \left \{ d(i,k)+d(k,j)+a_j-a_i|i<k<j \right \}
d(i,j)=min{d(i,k)+d(k,j)+aj−ai∣i<k<j}
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
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<j;k++){
ans=min(ans,dp(i,k)+dp(k,j)+a[j]-a[i]);
}
return ans;
}
int main()
{
#ifdef LOCAL
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif // LOCAL
while(cin >> 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;
}
括号序列(UVa1626):
定义如下正规括号序列(字符串):
- 空序列是正规括号序列。
- 如果S是正规括号序列,那么(S)和[S]也是正规括号序列。
- 如果A和B都是正规括号序列,那么AB也是正规括号序列。
例如,下面的字符串都是正规括号序列:(),[],(()),([]),()[],()[()],而如下字符串则不是正规括号序列:(,[,],)(,([()。
输入一个长度不超过100的,由“(”、“)”、“[”、“]”构成的序列,添加尽量少的括号,得到一个规则序列。如有多解,输出任意一个序列即可。
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#include <string>
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;
}
最大面积最小的三角剖分(UVa1331)
这实际是一个最优三角形剖分问题。
状态:设
d
(
i
,
j
)
d(i,j)
d(i,j)为子多边形
i
i
i ,
i
+
1
i+1
i+1 , … ,
j
j
j 的最优值(最大面积最小的三角形的最大面积),则边
i
−
j
i-j
i−j 在最优解中一定对应一个三角形
i
−
j
−
k
i-j-k
i−j−k (
i
<
k
<
j
i<k<j
i<k<j )。
状态转移方程:
d
(
i
,
j
)
=
m
i
n
{
m
a
x
{
d
(
i
,
k
)
,
d
(
k
,
j
)
,
w
(
i
,
j
,
k
)
}
∣
i
<
k
<
j
}
d(i,j)=min \left \{ max \left \{ d(i,k),d(k,j),w(i,j,k)\right \}|i<k<j \right \}
d(i,j)=min{max{d(i,k),d(k,j),w(i,j,k)}∣i<k<j}
原问题的解为
d
(
0
,
n
−
1
)
d(0,n-1)
d(0,n−1) 。
完整程序:
#define LOCAL
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring> //操作字符串和数组
#include <string>
#include <math.h>
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;
}