基础DP整理
- A - 免费馅饼 (HDU - 1176)
- B - (串长<=500)(HDU - 1159)
- C - (序列长度<=1000)(HDU - 1257)
- D - 记忆化搜索 (HDU - 1078)
- E - 01背包 (HDU - 2602)
- F - 完全背包 (HDU - 1114)
- G - 多重背包 (HDU - 2844)
- H - 背包问题1 (LibreOJ - 10179)
- I - 背包问题2 (CodeForces - 1458B)
- J - 区间dp1 (LibreOJ - 10147)
- K - 区间dp2 (黑暗爆炸 - 1260)
- L - 区间dp3 (HDU - 4283)
- M - 状压dp1 (CodeForces - 1051D)
- N - 状压dp2 (LibreOJ - 10170)
- O - 状压dp3 (HDU - 1400)
- P - 状压dp4 (CodeForces - 1209E1)
- Q - Gurdurr (Gym - 102341G)
A - 免费馅饼 (HDU - 1176)
用a[i][j]数组表示在第j秒的时候馅饼掉在了i - 1的位置上(下标总体偏移了,便于后续状态转移方程的表示),假设最后掉馅饼的时间为t,用dp[i][j]表示从第i个位置第j个时间段到t时间段能收获馅饼的最大值,利用逆推的思想,状态转移方程:dp[i][j] = max(max(dp[i][j + 1], dp[i - 1][j + 1]), dp[i + 1][j + 1]) + a[i][j];最后dp[6][0]就是最开始从坐标5出发,达到 t 时刻,每一秒只能移动一个位置,收获馅饼个数的最大值。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int a[15][100005], dp[15][100005];
int main(){
int n, p, q, t;
while(scanf("%d", &n)){
if(n == 0) break;
t = 0;
memset(a, 0, sizeof a);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i++){
scanf("%d%d", &p, &q);
a[p + 1][q]++;
if(t < q) t = q;
}
for(int i = 1; i <= 11; i++) dp[i][t] = a[i][t];
for(int j = t - 1; j >= 0; j--){
for(int i = 1; i <= 11; i++){
dp[i][j] = max(max(dp[i][j + 1], dp[i - 1][j + 1]), dp[i + 1][j + 1]) + a[i][j];
}
}
cout << dp[6][0] << '\n';
}
return 0;
}
B - (串长<=500)(HDU - 1159)
这道题是动态规划中最基本的最长公共子序列问题,最原始的做法(不加什么优化,但能ac)是嵌套两个for循环,枚举两个字符串,dp[i][j]表示字符串a前i个字符、字符串b前j个字符的最长公共子序列,状态转移方程:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); 如果a[i] == b[j],还有一种情况,此时还有状态转移方程:dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);最终dp[n][m]即为所求。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
char a[1005], b[1005];
int dp[1005][1005];
int main(){
// ios::sync_with_stdio(false);
// cin.tie(0), cout.tie(0);
while(scanf("%s %s", a + 1, b + 1) != EOF){
int n = strlen(a + 1);
int m = strlen(b + 1);
debug(m);
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if(a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
cout << dp[n][m] << '\n';
}
return 0;
}
C - (序列长度<=1000)(HDU - 1257)
这道题我不是用动态规划做的,我是用类似于贪心的做法,如果新来了一颗导弹,我就遍历所有导弹拦截系统,如果有能拦截它的导弹拦截系统(某个导弹拦截系统能拦截的高度比新来的导弹高),则更新这个拦截系统能拦截的最大高度;如果找不到,就再配置一个新的导弹拦截系统,放在所有拦截系统的后边。这样就保证了所有拦截系统能拦截的高度是从低到高排列的,实现了整体的有序,最后输出拦截系统的个数即可。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
int main(){
int a[100005], n, flag, s, i, j, b;
while(~scanf("%d", &n)){
s = 1;
scanf("%d", &b);
a[s] = b;
for(i = 1; i < n; i++){
flag = 0;
scanf("%d", &b);
for(j = 1; j <= s; j++){
if(a[j] >= b){
a[j] = b;
flag = 1;
break;
}
}
if(flag == 0){
s++;
a[s] = b;
}
}
printf("%d\n", s);
}
return 0;
}
D - 记忆化搜索 (HDU - 1078)
题意:一只老鼠吃奶酪,给你n * n的地图,地图上的数字表示奶酪的数量,老鼠一次能走最多k步,但是每次走到一个地方的奶酪数量要比之前的多,问,老鼠最多吃多少奶酪。
用dp保存数据和标记,用dfs搜索,起点是固定的,最后输出dfs(0, 0)即可。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int n, m;
int a[105][105], dp[105][105], ans, dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0};
int dfs(int x, int y){
if(dp[x][y]) return dp[x][y];
int s = 0;
for(int i = 0; i < 4; i++){
for(int j = 1; j <= m; j++){
int p = x + dx[i] * j, q = y + dy[i] * j;
if(p >= 0 && q >= 0 && p < n && q < n && a[p][q] > a[x][y]){
s = max(s, dfs(p, q));
}
}
}
dp[x][y] = s + a[x][y];
return dp[x][y];
}
int main(){
while(cin >> n >> m){
memset(dp, 0, sizeof dp);
if(n == -1 && m == -1) break;
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
cin >> a[i][j];
}
}
cout << dfs(0, 0) << '\n';
}
return 0;
}
E - 01背包 (HDU - 2602)
用一维数组表示最大价值的时候,枚举背包容量要从大到小枚举,因为01背包物品只有一个,容量是j - v[i]的价值f[j - v[i]]是前i - 1个物品中的价值,满足题意每个物品只能取一次的条件。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define inf 1e9 + 5
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int v[1005], w[1005], dp[1005];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t;
cin >> t;
while(t--){
memset(dp, 0, sizeof dp);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> w[i];
}
for(int j = 1; j <= n; j++){
cin >> v[j];
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j--){
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m] << '\n';
}
return 0;
}
F - 完全背包 (HDU - 1114)
完全背包问题跟01背包问题对容量的枚举方式不同,满足了每个物品可以取无限个的条件。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define inf 1e9 + 5
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
const int INF = 0x3f3f3f3f;
int v[10005], w[10005], dp[10005];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t;
cin >> t;
while(t--){
memset(v,0,sizeof(v));
memset(w,0,sizeof(w));
memset(dp, INF, sizeof dp);
int a, b, ans;
cin >> a >> b;
ans = b - a;
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
dp[0] = 0;
for(int i = 1; i <= n; i++){
for(int j = w[i]; j <= ans; j++){
dp[j] = min(dp[j], dp[j - w[i]] + v[i]);
}
}
if(dp[ans] != INF) cout << "The minimum amount of money in the piggy-bank is " << dp[ans] << "." << '\n';
else cout << "This is impossible.\n";
}
return 0;
}
G - 多重背包 (HDU - 2844)
用2的n次幂表示每个物品可以取的数量(最后凑不到的2的n次幂的自成一个数),把这几堆物品看成每一个背包,用01背包的做法就可以求得最终结果了。比模板多了唯一的点是查多少数字在题目要求的范围内出现过,枚举一遍看dp[i] == i 的个数即可。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define inf 1e9 + 7
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int v[105], s[105];
int dp[100005];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m;
while(cin >> n >> m){
memset(dp, 0, sizeof dp);
if(n == 0 && m == 0) break;
for(int i = 1; i <= n; i++) cin >> v[i];
for(int i = 1; i <= n; i++) cin >> s[i];
for(int i = 1; i <= n; i++){
int x[105], cnt = 0, u = 1;
while(1){
if(s[i] >= u){
x[++cnt] = u;
s[i] -= u;
u *= 2;
}
else break;
}
if(s[i] > 0) x[++cnt] = s[i];
for(int k = 1; k <= cnt; k++){
for(int j = m; j >= v[i] * x[k]; j--){
dp[j] = max(dp[j], dp[j - v[i] * x[k]] + v[i] * x[k]);
}
}
}
int ans = 0;
for(int i = 1; i <= m; i++){
if(dp[i] == i) ans++;
}
cout << ans << '\n';
}
return 0;
}
H - 背包问题1 (LibreOJ - 10179)
这道题也是多重背包问题,我的做法跟前面那道题一样。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define inf 1e9 + 7
void debug_out() {
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T) {
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
const int INF = 0x3f3f3f3f;
int n, m, b[205], c[205], dp[20005];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(dp, INF, sizeof dp);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> b[i];
}
for (int i = 1; i <= n; i++) {
cin >> c[i];
}
cin >> m;
memset(dp, INF, sizeof dp);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
int x[205], u = 1, cnt = 0;
while (1) {
if (c[i] >= u) {
x[++cnt] = u;
c[i] -= u;
u *= 2;
} else
break;
}
if (c[i] > 0) x[++cnt] = c[i];
for (int k = 1; k <= cnt; k++) {
for (int j = m; j >= x[k] * b[i]; j--) {
dp[j] = min(dp[j], dp[j - x[k] * b[i]] + x[k]);
}
}
}
cout << dp[m];
return 0;
}
I - 背包问题2 (CodeForces - 1458B)
题目大意:输入一个n,表示有n个瓶子。接下来有n行,每行第一个数代表该瓶子最大的容量,第二个数表示该瓶子当前的水量。你可以将一个瓶子的水倒入另一个瓶子中,假设到S的水,则另一个瓶子的当前容量应该为min(a[i], b[i] + s / 2),因为每次倒水都会损失一半,输出n个数,第 i 个数表示选其中的 i 个瓶子,这几个瓶子的含水量和最大是多少。
这是道背包DP问题。每个瓶子有选和不选两种状态,其他瓶子可以往这几个瓶子里倒水。一维的状态可以表示为f[i][j][k],表示前i个瓶子,从里面选j个,容量恰好为k时,水量的最大值是多少,在当前状态下,状态转移方程可以表示为:f[i][j][k] = f[i - 1][j - 1][k - a[i]] + b[i],当前状态只与上一层有关,所以可以优化掉一维,f[i][j] 表示前n个瓶子,选 i 个,当前容量为 j 时的水量最大值。就可以推出方程:f[i][j] = f[i - 1][j - a[i]] + b[i]。这个问题其实可以抽象成二维费用的背包问题,第一维的费用永远是1,也就是个数,第二维的费用是容量,所以直接拿二维费用的板子写,因为这里用的是恰好,所以把f[0][0] 初始化为0,其他为 -INF 即可。
为什么用恰好来表示状态,因为题目还有一个限制条件,输出选择 i 个的最大水量和,预处理一个A和B,表示容量总和和已有的水量总和,对于每个 i ,j从0 -> A, 每次res = max(res, min(j, f[i][j] + (B - f[i][j]) / 2)),整理一下,res = max(res, min(j, (B + f[i][j]) / 2)),然后每个 i 输出对应的res就好了。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int ansa, ansb;
int dp[105][100005], a[105], b[105];
int main(){
// ios::sync_with_stdio(false);
// cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i] >> b[i];
ansa += a[i], ansb += b[i];
}
memset(dp, -0x3f, sizeof dp);
dp[0][0] = 0;
for(int i = 1; i <= n; i++){
for(int j = i ; j >= 1; j--){
for(int k = ansa; k >= a[i]; k--){
dp[j][k] = max(dp[j][k], dp[j - 1][k - a[i]] + b[i]);
}
}
}
for(int i = 1; i <= n; i++){
double ans = 0;
for(int j = 0; j <= ansa; j++){
ans = max(ans, min(1.0 * j, 0.5 * (ansb + dp[i][j])));
}
printf("%.10lf ", ans);
}
return 0;
}
J - 区间dp1 (LibreOJ - 10147)
这道题是区间dp的版题,详情可以看我之前写的博客:区间DP
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define inf 1e9 + 7
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
int a[405], s[405], f1[405][405], f2[405][405];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
s[i] = s[i - 1] + a[i];
}
for(int i = n + 1; i < 2 * n; i++){
s[i] = a[i - n] + s[i - 1];
}
for(int len = 2; len <= n; len++){
for(int i = 1; i < 2 * n - len + 1; i++){
int l = i;
int r = i + len - 1;
f1[l][r] = 1e8;
f2[l][r] = 0;
for(int k = l; k < r; k++){
f1[l][r] = min(f1[l][r], f1[l][k] + f1[k + 1][r] + s[r] - s[l - 1]);
f2[l][r] = max(f2[l][r], f2[l][k] + f2[k + 1][r] + s[r] - s[l - 1]);
}
}
}
int minx = 1e8, maxx = 0;
for(int i = 1; i <= n; i++){
minx = min(minx, f1[i][i + n - 1]);
maxx = max(maxx, f2[i][i + n - 1]);
}
cout << minx << '\n' << maxx;
return 0;
}
K - 区间dp2 (黑暗爆炸 - 1260)
区间dp问题,开一个二维dp数组,dp[i][j] 代表区间从 i 到 j 最小染色次数。如果区间两端颜色相同 s[i] == s[j] 的话,dp[i][j] = min(dp[i][j - 1], dp[i + 1][j])。颜色不同 s[i] != s [j]的话,枚举中间端点k,把此区间分为两部分,转换为子问题求解即可。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
char a[55];
int dp[55][55];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> a + 1;
int s = strlen(a + 1);
memset(dp, 0x3f, sizeof dp);
for(int len = 1; len <= s; len++){
for(int l = 1; l <= s - len + 1; l++){
int r = l + len - 1;
if(len == 1) dp[l][r] = 1;
else if(a[l] == a[r]) dp[l][r] = min(dp[l + 1][r], dp[l][r - 1]);
else{
for(int k = l; k < r; k++){
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
}
}
cout << dp[1][s];
return 0;
}
L - 区间dp3 (HDU - 4283)
题目大意:有 n 个人,每个人有一个D[i]值,如果第 i 个人排在第 k 位置,则他的愤怒值就为D[i] *(k - 1),等待过程中有一个黑屋子,可以把人暂时放到黑屋子里,使总的愤怒值最小。
这是一道区间dp题,我们用dp[i][j]表示第i个人到第j个人unhappiness总和的最小值,因此dp[1][n]就是我们所要求的的值。怎么求呢?这种题肯定是转换成一些子问题,不断地的递推求解。我们来考虑第i个人的情况,在区间[i, j]中,第i个人可能是第一个走的,也可能是第j - i + 1个走的,所以假设第i个人是第k个走的,根据题目有第i个人的这里写代码片unhappiness值为(k - 1) * D[i]。那么从i + 1开始的k - 1个人都是在i 之前走的,此时就会出现一个子问题dp[i + 1][i + 1 + k - 1 + 1]。这个区间的人都是已经走过的,那么在[i, j] 区间里,从i + k到j都是还没有走的,那么他们就要继续等待,把它们看成一个整体,即unhappyiness 值为k * (sum[j] - sum[i + k - 1])。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
#define INF 0x3f3f3f3f
int a[105], s[105], dp[105][105];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t;
cin >> t;
for(int u = 1; u <= t; u++){
memset(s, 0, sizeof s);
memset(dp, 0, sizeof dp);
int n;
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
s[i] += s[i - 1] + a[i];
}
for(int i = 1; i <= n; i++){
for(int j = i + 1; j <= n; j++){
dp[i][j] = INF;
}
}
for(int len = 1; len <= n; len++){
for(int i = 1; i <= n - len + 1; i++){
int j = i + len - 1;
for(int k = i; k <= j; k++){
dp[i][j] = min(dp[i][j], dp[i + 1][k] + (k - i) * a[i] + (k - i + 1) * (s[j] - s[k]) + dp[1 + k][j]);
}
}
}
cout << "Case #" << u << ": " << dp[1][n] << endl;
}
return 0;
}
M - 状压dp1 (CodeForces - 1051D)
第 i 列的状态有四种:(黑,黑),(黑,白),(白,黑),(白,白),设为0(0, 0), 1(0, 1), 2(1, 0), 3(1, 1)。dp[i][k][j]:i 表示第 i 列,k 表示有 k 种,j 表示第 i 列的状态。
那么我们可以得到:dp[i][k][0] = dp[i - 1][k][0] + dp[i - 1][k][1] + dp[i - 1][k][2] + dp[i - 1][k-1][3]。
同理:dp[i][k][3] = dp[i - 1][k][3] + dp[i - 1][k][1] + dp[i - 1][k][2] + dp[i - 1][k - 1][0]。
因为第 i 列的状态是纯色的,只要上一列中有和这一列一样的颜色就可以叠加并且 k 不变,如果和上一个完全不一样,则种类会多加一个,所以要取k - 1。
dp[i][k][1] = dp[i-1][k][1] + dp[i-1][k-2][2] + dp[i-1][k-1][0] + dp[i-1][k-1][3]。
同理:dp[i][k][2] = dp[i-1][k][2] + dp[i-1][k-2][1] + dp[i-1][k-1][0] + dp[i-1][k-1][3]。
因为第 i 列的状态是杂色的,所以要考虑如果上一个和当前状态一样则加上,如果完全不一样则要找k - 2的状态加上,如果是纯色会多一种,则找k - 1的状态加上。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
const int mod = 998244353;
LL dp[1005][2005][4];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, k;
cin >> n >> k;
dp[1][1][0] = 1;
dp[1][2][1] = 1;
dp[1][2][2] = 1;
dp[1][1][3] = 1;
debug(dp[n][k][1]);
for(int i = 2; i <= n; i++){
dp[i][1][1] = dp[i - 1][1][1];
dp[i][1][0] = dp[i - 1][1][0];
dp[i][1][2] = dp[i - 1][1][2];
dp[i][1][3] = dp[i - 1][1][3];
for(int j = 2; j <= k; j++){
dp[i][j][0] = (dp[i - 1][j][0] + dp[i - 1][j][1] + dp[i - 1][j][2] + dp[i - 1][j - 1][3]) % mod;
dp[i][j][1] = (dp[i - 1][j][1] + dp[i - 1][j - 1][0] + dp[i - 1][j - 2][2] + dp[i - 1][j - 1][3]) % mod;
dp[i][j][2] = (dp[i - 1][j][2] + dp[i - 1][j - 1][0] + dp[i - 1][j - 2][1] + dp[i - 1][j - 1][3]) % mod;
dp[i][j][3] = (dp[i - 1][j][3] + dp[i - 1][j - 1][0] + dp[i - 1][j][1] + dp[i - 1][j][2]) % mod;
}
}
int ans;
ans = (dp[n][k][0] + dp[n][k][1] + dp[n][k][2] + dp[n][k][3]) % mod;
cout << ans;
return 0;
}
N - 状压dp2 (LibreOJ - 10170)
因为枚举i - 1层的状态和第i层的状态所需的循环过多导致时间复杂度很高,所以在这里我们运用预处理的方式来解决此题,用st数组来存储不存在连续个1的合法方案数,用head数组来存储与合法方案数进行’|’和’&’运算后的合法方案数。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
const int N = 12, M = 1 << 10, K = 105;
vector<int> st;
int cnt[M], n, k;
vector<int> h[M];
LL f[N][K][M];
int check(int st){
for(int i = 0; i < n; i++){
if((st >> i & 1) && (st >> i + 1 & 1)) return 0;
}
return 1;
}
int count(int st){
int ans = 0;
for(int i = 0; i < n; i++) ans += st >> i & 1;
return ans;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> k;
for(int i = 0; i < 1 << n; i++){
if(check(i)){
st.push_back(i);
cnt[i] = count(i);
}
}
for(int i = 0; i < st.size(); i++){
for(int j = 0; j < st.size(); j++){
int a = st[i], b = st[j];
if((a & b) == 0 && check(a | b)) h[i].push_back(j);
}
}
f[0][0][0] = 1;
for(int i = 1; i <= n + 1; i++){
for(int j = 0; j <= k; j++){
for(int a = 0; a < st.size(); a++){
for(int b: h[a]){
int c = cnt[st[a]];
if(j >= c) f[i][j][st[a]] += f[i - 1][j - c][st[b]];
}
}
}
}
cout << f[n + 1][k][0] << '\n';
return 0;
}
O - 状压dp3 (HDU - 1400)
集合:前i - 1列已经摆好,前i - 1列摆放的横着的砖头延伸到第i列的状态j的方案数(第i - 1列为砖块的起始位置)
条件:1、空格必须是偶数个 2、不能存在冲突
用k来表示第i - 2列的砖块延伸到第i - 1列的状态,j来表示从第i - 1列延伸到第i列的方案,(因为砖块横着摆放的长度是2,所以k也就是把砖块头放在第i - 2列的状态,因为长度为2,所以砖块尾会延伸到第i - 1列,j的表示也是同理)。如果此时j的状态是已经固定了的,即前i -1 列摆放着的横着的砖头延伸到第i列的状态已经确定了,但是第i - 2列横着摆放的砖头延伸到第i - 1列的状态还没有确定,那么我们就开始枚举第i - 2列摆放横着的砖头延伸到第i - 1列的状态,即k的状态,这里我们用二进制来表示k的状态,因为横着的砖头摆放完后,便要摆放竖着的砖头,所以必须要确保能装下竖着的摆放砖头。因为竖着的砖头的长度为1,所以横着摆放砖头的状态中间必须保证要有偶数个空位才行。
首先第i - 1列存有的砖头是j和k状态的叠加,因为砖块头和砖块尾巴都在第i - 1列上,又因为j和k都是用二进制表示的,所以j和k的二进制数的1叠加后(即进行异或操作后),必须存有连续偶数个0,如果符合条件那么就可以证明能从第i - 2列延伸到第i - 1列的状态k可以转移到从第i - 1列延伸到第i列的状态k,所以此时可以得到状态转移方程:f[i][j] = f[i][j] + f[i - 1][k];
为什么不需要判断j和k的状态呢?
因为第f[i - 1][k]表示的是前i - 2列已经摆好,且前i - 2列摆放的横着的砖头延伸到第i - 2列的状态j的方案数,所以f[i - 1][k]存的必定是合法的方案数,所以第i - 2行的空位肯定是偶数位。(f[i - 1, k]状态判断的合法条件是第i - 3列的状态k与第i - 1列的状态j的在第i - 2列中的异或值的连续零为偶数)。那么j呢,如果第i列枚举完后,那么就开始判断第i + 1列了,因为第i + 1列判断的是第i列中第i - 1列延伸到第i的砖块的状态和第i列延伸到第i + 1列的状态的异或值不发生冲突,所以第 j 也无需判断。
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
#define m_p make_pair
const int N = 12, M = 1 << N;
LL f[N][M];
int st[M];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m;
while(cin >> n >> m){
if(n == 0 && m == 0) break;
for(int i = 0; i < 1 << n; i++){
int cnt = 0;
st[i] = 1;
for(int j = 0; j < n; j++){
if(i >> j & 1){
if(cnt & 1){
st[i] = 0;
break;
}
cnt = 0;
}
else cnt++;
}
if(cnt & 1) st[i] = 0;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for(int i = 1; i <= m; i++){
for(int j = 0; j < 1 << n; j++){
for(int k = 0; k < 1 << n; k++){
if((j & k) == 0 && st[j | k]) f[i][j] += f[i - 1][k];
}
}
}
cout << f[m][0] << '\n';
}
return 0;
}
P - 状压dp4 (CodeForces - 1209E1)
具体的实现代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
void debug_out(){
cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
cerr << " " << to_string(H);
debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif
const int N = 205;
int a[N][N], dp[N][20];
int main(){
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t, n, m;
cin >> t;
while(t--){
memset(dp, 0, sizeof dp);
cin >> n >> m;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
cin >> a[i][j];
}
}
for(int i = 1; i <= m; i++){
for(int s = 0; s < (1 << n); s++){
for(int k = 1; k <= n; k++){
int c = 0;
for(int j = 1; j <= n; j++){
if(s & (1 << j - 1)) c += a[j][i];
}
for(int p = 0; p < (1 << n); p++){
if(!(s & p)) dp[i][s | p] = max(dp[i][s | p], dp[i - 1][p] + c);
}
for(int j = 1; j <= n; j++){
a[j - 1][i] = a[j][i];
}
a[n][i] = a[0][i];
}
}
}
cout << dp[m][(1 << n) - 1] << '\n';
}
return 0;
}
Q - Gurdurr (Gym - 102341G)
这道题是2019年ccpc秦皇岛站的题目,具体涉及到基于连通性状态压缩的动态规划(轮廓线DP / 插头DP),这类DP的做法请见我另外一篇博客:插头DP
具体代码有时间再补…