背包问题
01背包问题
思路:
01背包问题,表示每个物品要么放,要么不放。从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]
表示价值的最大值。状态计算,因为每个状态可以表示为选择当前的物品,或者不选当前的物品,二者价值取最大值即可,即状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
v
i
]
+
w
i
)
,
j
≥
v
i
f[i][j] = max(f[i - 1][j], f[i - 1][j - v_i]+w_i), j \ge v_i
f[i][j]=max(f[i−1][j],f[i−1][j−vi]+wi),j≥vi
朴素代码:
#include<iostream>
using namespace std;
const int N = 1005;
int n, m, v[N], w[N], f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if (j >= v[i])
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m];
return 0;
}
一维优化
f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
变化为
f[j] = f[j] // 省略
f[j] = max(f[j], f[j - v[i]] + w[i])
// 由于f[i][j]需要用到第i - 1层的结果,j-v[i]严格小于j,所以用j-v[i]更新j时,用的是第i层的结果,因此j需要逆序一下,
优化代码:
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main()
{
int n, V;
cin >> n >> V;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for (int j = V; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
cout << f[V] << endl;
return 0;
}
完全背包问题
思路:
完全背包问题,每一个物品可以选任意数量个,从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]
表示价值的最大值。状态计算,因为每个状态可以表示为选0个,选1个…选k个,即朴素状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
k
×
v
i
]
+
k
×
w
i
)
,
j
≥
k
×
v
i
f[i][j] = max(f[i - 1][j], f[i - 1][j - k ×v_i]+k × w_i), j \ge k ×v_i
f[i][j]=max(f[i−1][j],f[i−1][j−k×vi]+k×wi),j≥k×vi
f[i - 1][j]
为k = 0
时候的特殊情况
朴素代码:
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
// f[i][j] = f[i - 1][j];
for (int k = 0; k * v[i] <= m; k++)
{
if (j >= k * v[i]) f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
cout << f[n][m] << endl;
return 0;
}
由于是三重循环,时间复杂度为 O ( n 3 ) O(n^3) O(n3),会超时,下面进行优化
f[i][j] = max(f[i - 1][j], f[i - 1][j - vi] + wi, f[i - 1][j - 2vi] + 2wi,...f[i - 1][j - kvi] + kwi)
f[i][j - vi] = max( f[i - 1][j - vi] , f[i - 1][j - 2vi] + wi,...f[i - 1][j - kvi] + (k - 1)wi)
状态转移方程为
f[i][j] = max(f[i - 1][j], f[i][j - vi] + wi)
优化代码1:
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j];
if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
一维优化
f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
变化为
f[j] = f[j] // 省略
f[j] = max(f[j], f[j - v[i]] + w[i])
// 由于f[i][j]需要用到第i层的结果,j-v[i]严格小于j,所以用j-v[i]更新j时,用的是第i层的结果,因此j不需要逆序,这一点与01背包代码相反。
优化代码2:
#include<iostream>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j++)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
cout << f[m] << endl;
return 0;
}
多重背包问题1
思路:
多重背包问题在于每个物品选择的个数是有限个,从集合的角度分析DP问题,状态表示为:选择前i个物品,总体积小于等于j的选法的集合,属性f[i][j]
表示价值的最大值。状态计算,因为每个状态可以表示为选0个,选1个…选k个,k小于等于当前物品的最大个数,即状态转移方程为:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
]
[
j
]
,
f
[
i
−
1
]
[
j
−
k
∗
v
[
i
]
]
+
k
∗
w
[
i
]
)
,
k
≤
s
[
i
]
,
k
∗
v
[
i
]
≤
j
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]) ,k \le s[i], k * v[i] \le j
f[i][j]=max(f[i][j],f[i−1][j−k∗v[i]]+k∗w[i]),k≤s[i],k∗v[i]≤j
代码:
#include <iostream>
using namespace std;
const int N = 110;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
cout << f[n][m] << endl;
return 0;
}
多重背包问题2
思路:
这道题的数据量更大,朴素的做法会超时,接下来要对它进行优化,我们知道,任意一个整数都可以用二进制的数来进行表示。
123 = 1 + 2 + 4 + 8 + 16 + 32 + 50
1 ~ 32可以凑出 1 ~ 63 加上 50 可以凑出 51 ~ 123,
所以可以凑出 1 ~ 123内的任意数
所以本题优化在于将一类物品分为由若干个数为二进制数的物品组合而成的大物品,再由大物品拼凑而成特定重量的物品,这样就优化为一个01背包问题了。
代码:
#include<iostream>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M];
int main()
{
cin >> n >> m;
int cnt = 0; //分组的序号
for(int i = 1;i <= n;i ++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 分组里面的个数
while(k <= s)
{
cnt ++ ;
v[cnt] = a * k ; //分组整体体积
w[cnt] = b * k; // 分组整体价值
s -= k;
k *= 2;
}
//剩余的一组
if(s > 0)
{
cnt ++ ;
v[cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt ;
for(int i = 1; i <= n ;i ++)
for(int j = m; j >= v[i]; j --)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
分组背包问题
思路:
与01背包问题很像,区别在于分组背包里面只能选择一个物品,所以只用在01背包里再套一层循环,选一个可以使总价值最大的即可。
代码:
#include <iostream>
using namespace std;
const int N = 110;
int v[N][N], w[N][N], s[N];
int f[N];
int n, m;
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
for (int j = 1; j <= s[i]; j++)
cin >> v[i][j] >> w[i][j];
}
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 1; k <= s[i]; k++)
if (j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}
线性DP
数字三角形
思路(自顶向下):
从集合的角度考虑DP问题,状态表示为:从起点到[i, j]点所有路径的集合,属性dp[i][j]
表示最大的数字和,每一个状态可以由该点上面的两个状态中的最大值加上该点的值f[i][j]
转移过来,因此从上至下来看,状态转移方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
1
]
,
d
p
[
i
−
1
]
[
j
]
)
+
f
[
i
]
[
j
]
dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + f[i][j]
dp[i][j]=max(dp[i−1][j−1],dp[i−1][j])+f[i][j]
由于数字的值有负数的情况,所以要初始化三角形及轮廓为一个很小的负数。最后结果在最后一行,取最大值即可
代码:
#include<iostream>
using namespace std;
const int N = 510, inf = -0x3f3f3f3f;
int f[N][N];
int dp[N][N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> f[i][j];
for (int i = 0; i <= n; i++)
for (int j = 0; j <= i + 1; j++)
dp[i][j] = inf;
dp[1][1] = f[1][1];
for (int i = 2; i <= n; i++)
for (int j = 1; j <= i; j++)
{
dp[i][j] = max(dp[i - 1][j - 1], dp[i - 1][j]) + f[i][j];
}
int res = inf;
for (int i = 1; i <= n; i++) res = max(res, dp[n][i]);
cout << res << endl;
return 0;
}
思路(自底向上):
从最后一行往上加,每一个状态可以由下面的两个状态来表示,即状态转移方程为:
f
[
i
]
[
j
]
=
f
[
i
]
[
j
]
+
m
a
x
(
f
[
i
+
1
]
[
j
]
,
f
[
i
+
1
]
[
j
+
1
]
)
f[i][j] = f[i][j] + max(f[i+1][j], f[i+1][j+1])
f[i][j]=f[i][j]+max(f[i+1][j],f[i+1][j+1])
代码:
#include<iostream>
using namespace std;
const int N = 510;
int f[N][N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> f[i][j];
for (int i = n - 1; i >= 1; i--)
for (int j = i; j >= 1; j--)
f[i][j] += max(f[i + 1][j], f[i + 1][j + 1]);
cout << f[1][1] << endl;
return 0;
}
最长上升子序列
思路:
从集合的角度考虑DP问题,状态表示为:所有以第i个数字结尾的上升子序列,属性f[i]
表示所有以第i个数字结尾的上升子序列的长度的最大值。枚举每个数字为结尾,接着枚举以该数字为结尾的上升子序列,如果结尾大于子序列中的值,就对当前状态进行转移,状态转移方程为:
f
[
i
]
=
m
a
x
(
f
[
i
]
,
f
[
j
]
+
1
)
,
a
[
i
]
>
a
[
j
]
f[i] = max(f[i], f[j] + 1), a[i] \gt a[j]
f[i]=max(f[i],f[j]+1),a[i]>a[j]
在枚举结尾的过程中保存上升子序列长度的最大值:
代码:
#include<iostream>
using namespace std;
const int N = 1010;
int a[N], f[N];
int main()
{
int n, ans = 1;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++)
{
// 初始情况 长度为1
f[i] = 1;
for (int j = 1; j < i; j++)
{
if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
}
ans = max(ans, f[i]);
}
cout << ans << endl;
return 0;
}
最长公共子序列
思路:
从集合的角度考虑DP问题,状态表示为:字符串A前i个字符,字符串B前j个字符组成的公共子序列,属性f[i][j]
表示最长公共子序列的长度的最大值。
状态计算:每一个状态可以由字符串A前i-1个字符,字符串B前j-1个字符;字符串A前i-1个字符,字符串B前j个字符;字符串A前i个字符,字符串B前j-1个字符转移过来,如果a[i] == b[j]
,f[i][j] = f[i - 1][j - 1] + 1
,否则取f[i - 1][j]、f[i][j - 1]
的最大值,状态转移方程为:
f
[
i
]
[
j
]
=
{
f
[
i
−
1
]
[
j
−
1
]
+
1
a
[
i
]
=
b
[
j
]
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
]
[
j
−
1
]
)
a
[
i
]
≠
b
[
j
]
f[i][j]= \begin{cases} f[i-1][j-1] + 1 & a[i] = b[j] \\ max(f[i-1][j], f[i][j-1]) & a[i] \neq b[j] \\ \end{cases}
f[i][j]={f[i−1][j−1]+1max(f[i−1][j],f[i][j−1])a[i]=b[j]a[i]=b[j]
代码
#include<iostream>
using namespace std;
const int N = 1005;
char a[N], b[N];
int f[N][N];
int n, m;
int main()
{
cin >> n >> m >> a + 1 >> b + 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
}
}
cout << f[n][m] << endl;
return 0;
}
最短编辑距离
思路:
从集合的角度考虑DP问题,状态表示为所有将a[1~i]
变化为b[1~j]
的操作方式的集合,属性f[i][j]
表示操作次数的最小值。因为增添字符、删除字符都是相对而言的,所以可以以一个字符串作为基准,不妨以字符串a作为基准。对于增加操作,字符串a通过增加一个字符变化为字符串b,则a[1 ~ i] == b[1 ~ j - 1]
,对应操作次数为f[i][j - 1] + 1
;对于删除操作,字符串a通过增加一个字符变化为字符串b,则a[1 ~ i - 1] == b[1 ~ j ]
,对应操作次数为f[i - 1][j] + 1
;对于修改操作,字符串a通过修改一个字符变化为字符串b,如果a[i] == b[j]
不用修改,对应操作次数为f[i - 1][j - 1]
,否则需要修改,对应操作次数为f[i - 1][j - 1] + 1
。每一个状态都可以从上面三个状态转移过来,因此状态转移方程为:
f
[
i
]
[
j
]
=
m
i
n
(
f
[
i
]
[
j
−
1
]
+
1
,
f
[
i
−
1
]
[
j
]
+
1
,
f
[
i
−
1
]
[
j
−
1
]
+
(
a
[
i
]
≠
b
[
j
]
)
)
f[i][j] = min(f[i][j - 1] + 1, f[i - 1][j] + 1,f[i - 1][j - 1] + (a[i]\ne b[j]))
f[i][j]=min(f[i][j−1]+1,f[i−1][j]+1,f[i−1][j−1]+(a[i]=b[j]))
代码:
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main()
{
cin >> n >> a + 1;
cin >> m >> b + 1;
// 如果a空 只能通过添加字符使 a == b, b的长度为几,就有几个添加字符的操作
for (int i = 0; i <= m; i++) f[0][i] = i;
// 如果b空 只能通过删除字符使 a == b, a的长度为几,就有几个删除字符的操作
for (int i = 0; i <= n; i++) f[i][0] = i;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
// a[1,i-1] = b[1,j] a通过删除字符 a[1,i] = b[1,j-1] a通过增加字符
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
// 修改字符 若a[i] == b[j] 不用修改
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
// 修改字符的操作 + 1
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
}
cout << f[n][m] << endl;
return 0;
}
编辑距离
思路:
根据上一道题的思路,封装成一个计算编辑距离的函数,计算询问中的每个字符串与给定字符串的编辑距离,如果小于等于给定的操作次数,计数器+1,最后输出结果即可。
代码:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 15, M = 1010;
int f[N][N];
char str[M][N];
int n, m;
int edit_dist(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
// 如果a空 a只能通过添加字符使 a == b, b的长度为几,就有几个添加字符的操作
for (int i = 0; i <= lb; i++) f[0][i] = i;
// 如果b空 a只能通过删除字符使 a == b, a的长度为几,就有几个删除字符的操作
for (int i = 0; i <= la; i++) f[i][0] = i;
for (int i = 1; i <= la; i++)
{
for (int j = 1; j <= lb; j++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
}
}
return f[la][lb];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%s", str[i] + 1);
while (m--)
{
char s[N];
int limit;
scanf("%s%d", s + 1, &limit);
int res = 0;
for (int i = 1; i <= n; i++)
if (edit_dist(str[i], s) <= limit)
res++;
printf("%d\n", res);
}
return 0;
}
区间DP
石子合并
思路:
从集合的角度考虑DP问题,状态表示为:从i到j堆石子的所有合并方式,属性f[i][j]
表示从
i
i
i到
j
j
j堆石子合并的最小代价。状态计算:根据每个不同合并石子堆的分界线可以划分出不同的状态。合并从i到j堆石子的状态可以转移为合并从i到k堆石子,合并从
k
+
1
k+1
k+1到
j
j
j堆石子,合并这两堆石子的代价之和,其中k表示分界线,
k
∈
[
i
,
j
)
k \in [i,j)
k∈[i,j),可以用前缀和数组处理区间和,最后得到状态转移方程:
f
[
i
]
[
j
]
=
m
i
n
(
f
[
i
]
[
k
]
+
f
[
k
+
1
]
[
j
]
+
s
[
j
]
−
s
[
l
−
1
]
)
,
k
∈
[
i
,
j
)
f[i][j] = min(f[i][k]+f[k+1][j]+s[j]-s[l-1]),k \in [i,j)
f[i][j]=min(f[i][k]+f[k+1][j]+s[j]−s[l−1]),k∈[i,j)
区间DP问题一般都要枚举区间的长度。
代码
#include<iostream>
using namespace std;
const int N = 310;
int s[N], f[N][N], a[N];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], s[i] = s[i - 1] + a[i];
//len 表示区间长度
for (int len = 2; len <= n; len++)
{
for (int i = 1; i + len - 1 <= n; i++)
{
int l = i, r = i + len - 1;
f[l][r] = 1e8;
for (int k = l; k < r; k++)
// k表示分界线
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
cout << f[1][n] << endl;
return 0;
}
计数类DP
整数划分
思路(完全背包):
本题可以看作体积为
n
n
n,物品重量为
[
1
,
n
]
[1,n]
[1,n]的
n
n
n件物品的完全背包问题。状态表示为:从1到i当中选,总体积恰好为j的所有方案的集合。f[i][j]
表示所有选法的数量。集合的划分可以根据第i个物品选择多少个,把所有集合的方案数相加,得到朴素状态转移方程:
f
[
i
]
[
j
]
=
f
[
i
]
[
j
]
+
f
[
i
−
1
]
[
j
−
k
∗
i
]
,
k
×
i
≤
j
f[i][j] = f[i][j] + f[i - 1][j - k * i], k×i \le j
f[i][j]=f[i][j]+f[i−1][j−k∗i],k×i≤j
代码:
#include<iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];
int main()
{
scanf("%d", &n);
f[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= n; j++)
for (int k = 0; k * i <= j; k++)
f[i][j] = (f[i][j] + f[i - 1][j - k * i]) % mod;
printf("%d", f[n][n]);
return 0;
}
优化代码1
#include<iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];
int main()
{
scanf("%d", &n);
f[0][0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= n; j++)
{
f[i][j] = f[i - 1][j];
if (j >= i) f[i][j] = (f[i][j] + f[i][j - i]) % mod;
}
printf("%d", f[n][n]);
return 0;
}
优化代码2
#include<iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];
int main()
{
scanf("%d", &n);
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
f[j] = (f[j] + f[j - i]) % mod;
printf("%d", f[n]);
return 0;
}
状态压缩DP
最短Hamilton路径
思路
首先想下暴力算法,这里直接给出一个例子。
比如数据有
5
5
5 个点,分别是
0
,
1
,
2
,
3
,
4
0,1,2,3,4
0,1,2,3,4
那么在爆搜的时候,会枚举一下六种路径情况(只算对答案有贡献的情况的话):
c
a
s
e
1
:
0
→
1
→
2
→
3
→
4
case 1: 0→1→2→3→4
case1:0→1→2→3→4
c
a
s
e
2
:
0
→
1
→
3
→
2
→
4
case 2: 0→1→3→2→4
case2:0→1→3→2→4
c
a
s
e
3
:
0
→
2
→
1
→
3
→
4
case 3: 0→2→1→3→4
case3:0→2→1→3→4
c
a
s
e
4
:
0
→
2
→
3
→
1
→
4
case 4: 0→2→3→1→4
case4:0→2→3→1→4
c
a
s
e
5
:
0
→
3
→
1
→
2
→
4
case 5: 0→3→1→2→4
case5:0→3→1→2→4
c
a
s
e
6
:
0
→
3
→
2
→
1
→
4
case 6: 0→3→2→1→4
case6:0→3→2→1→4
那么观察一下
c
a
s
e
1
case 1
case1 和
c
a
s
e
3
case 3
case3,可以发现,我们在计算从点
0
0
0 到点
3
3
3的路径时,其实并不关心这两中路径经过的点的顺序,而是只需要这两种路径中的较小值,因为只有较小值可能对答案有贡献。
所以,我们在枚举路径的时候,只需要记录两个属性:当前经过的点集,当前到了哪个点。
而当前经过的点集不是一个数。观察到数据中点数不会超过
20
20
20,我们可以用一个二进制数表示当前经过的点集。其中第
i
i
i位为 1/0
表示是/否经过了点
i
i
i。
然后用闫式
d
p
dp
dp 分析法考虑
d
p
dp
dp
状态表示:
f
[
s
t
a
t
e
]
[
j
]
f[state][j]
f[state][j]。其中
s
t
a
t
e
state
state 是一个二进制数,表示点集的方法如上述所示。
- 集合:经过的点集为 s t a t e state state,且当前到了点 j j j 上的所有路径。
- 属性:路径总长度的最小值
状态计算:假设当前要从点
k
k
k 转移到
j
j
j。那么根据
H
a
m
i
l
t
o
n
Hamilton
Hamilton 路径的定义,走到点
k
k
k 的路径就不能经过点
j
j
j,所以就可以推出状态转移方程
f
[
s
t
a
t
e
]
[
j
]
=
m
i
n
(
f
[
s
t
a
t
e
]
[
j
]
,
f
[
s
t
a
t
e
−
(
1
<
<
j
)
]
[
k
]
+
w
[
k
]
[
j
]
)
f[state][j] = min(f[state][j], f[state - (1 << j)][k] + w[k][j])
f[state][j]=min(f[state][j],f[state−(1<<j)][k]+w[k][j])
其中w[k][j]
表示从点
k
k
k 到点 &j& 的距离
state - (1 << j)
表示从点集中去除到
j
j
j的点
所有状态转移完后,根据
f
[
s
t
a
t
e
]
[
j
]
f[state][j]
f[state][j]的定义,要输出
f
[
111
⋯
11
(
n
个
1
)
]
[
n
−
1
]
f[111⋯11(n个1)][n−1]
f[111⋯11(n个1)][n−1]。
那么怎么构造
n
n
n 个
1
1
1 呢,可以直接通过 1 << n
求出
100
⋯
0
(
n
个
0
)
100⋯0(n个0)
100⋯0(n个0),然后减一即可。
时间复杂度
枚举所有
s
t
a
t
e
state
state 的时间复杂度是
O
(
2
n
)
O(2^n)
O(2n)
枚举
j
j
j 的时间复杂读是
O
(
n
)
O(n)
O(n)
枚举
k
k
k 的时间复杂度是
O
(
n
)
O(n)
O(n)
所以总的时间复杂度是
O
(
n
2
2
n
)
O(n^22^n)
O(n22n)
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20, M = 1 << N;
// f[i][j]所有从0到j,走过的路径包括i的路径 i为路径的集合
int w[N][N], f[M][N];
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
cin >> w[i][j];
memset(f, 0x3f, sizeof f);
// 表示从0到0 0,2,3,4状压为11101
f[1][0] = 0;
for (int i = 0; i < 1 << n; i++)
for (int j = 0; j < n; j++)
{
// i中包括到j的路径
if (i >> j & 1)
{
for (int k = 0; k < n; k++)
{
// i中去除到j的路径 包括k的路径
if ((i - (1 << j)) >> k & 1)
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
}
}
}
cout << f[(1 << n) - 1][n - 1] << endl;
return 0;
}
树形DP
没有上司的舞会
思路:
根据闫式DP分析法的思想,状态表示为:
f
[
u
]
[
0
]
f[u][0]
f[u][0]表示从以
u
u
u为根节点的子树中选择,并且不包括
u
u
u的选法,则
f
[
u
]
[
1
]
f[u][1]
f[u][1]表示从以
u
u
u为根节点的子树中选择,并且包括
u
u
u的选法。
f
[
u
]
[
0
]
f[u][0]
f[u][0]、
f
[
u
]
[
1
]
f[u][1]
f[u][1]值为结果的最大值。再来计算状态转移,
f
[
u
]
[
0
]
f[u][0]
f[u][0]可以由其子孙节点
j
j
j转移来,由于不包括
u
u
u,可以从
f
[
j
]
[
0
]
f[j][0]
f[j][0]、
f
[
j
]
[
1
]
f[j][1]
f[j][1],转移到
f
[
u
]
[
0
]
f[u][0]
f[u][0];
f
[
u
]
[
1
]
f[u][1]
f[u][1],由于包括
u
u
u,所以只能由
f
[
j
]
[
0
]
f[j][0]
f[j][0]转移过来。则状态转移方程为
{
f
[
u
]
[
0
]
=
f
[
u
]
[
0
]
+
m
a
x
(
f
[
j
]
[
0
]
,
f
[
j
]
[
1
]
)
f
[
u
]
[
1
]
=
f
[
u
]
[
1
]
+
f
[
j
]
[
0
]
\begin{cases} f[u][0] = f[u][0] + max(f[j][0], f[j][1]) \\ f[u][1] = f[u][1] + f[j][0] \end{cases}
{f[u][0]=f[u][0]+max(f[j][0],f[j][1])f[u][1]=f[u][1]+f[j][0]
枚举所有节点时间复杂度等于边的个数,时间复杂度为
O
(
n
)
O(n)
O(n),也是本题的时间复杂度。
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 6010;
int happy[N];
int h[N], e[N], ne[N], idx;
int f[N][2];
bool has_father[N];
//邻接表存储图
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
f[u][1] = happy[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> happy[i];
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++)
{
int l, k;
cin >> l >> k;
has_father[l] = true;
add(k, l);
}
// 寻找根节点
int root = 1;
while(has_father[root]) root++;
dfs(root);
cout << max(f[root][0], f[root][1]) << endl;
return 0;
}
记忆化搜索
有关记忆化搜索的介绍:https://blog.csdn.net/hjf1201/article/details/78680814
滑雪
思路:
根据根据闫式DP分析法的思想,状态表示为:所有从
(
i
,
j
)
(i,j)
(i,j)开始划的路径,
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示路径长度的最大值。每一个状态都可以由上下左右四个方向转移过来,因此可以用方向数组表示转移的过程,若某个方向的高度低于当前状态的高度,则可以转移过来,每找到一个路径的长度就直接返回。由于高度存在为0的值,
f
f
f初始化为-1。
代码:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];
int dx[4] = {-1, 0, 1 ,0}, dy[4] = {0, 1, 0, -1};
int n, m;
int dp(int x, int y)
{
int &v = f[x][y];
// 不为-1,也就是计算过的 直接返回
if (~v) return v;
// 计算路径长度 初始化为1
v = 1;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
// 不出边界 并且下一个方向的高度低于当前方向的
if (a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y])
// 下一个方向的路径长度加1,取最大
v = max(v, dp(a, b) + 1);
}
// 返回当前路径长度
return v;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> h[i][j];
memset(f, -1, sizeof f);
int res = 0;
// 由于不知道那个点为起点 所以依次进行计算
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
res = max(res, dp(i, j));
cout << res << endl;
return 0;
}