# 「笔记」DP简单笔记

## 线性DP

### 【例题】AcWing271 杨老师的照相排列

#### 状态：

$$f_{a_1,a_2,a_3,a_4,a_5}$$ 表示各排从左端起点分别站了 $$a_1,a_2,a_3,a_4,a_5$$ 个人时，合影方案数量，$$k<5$$ 的排用 $$0$$ 替代即可。

#### 边界：

$$f_{0,0,0,0,0}=1$$

#### 转移：

$$a_1< N_1$$，那么令 $$f_{a_1+1,a_2,a_3,a_4,a_5}+=f_{a_1,a_2,a_3,a_4,a_5}$$

$$a_2<N_2$$，那么令 $$f_{a_1,a_2+1,a_3,a_4,a_5}+=f_{a_1,a_2,a_3,a_4,a_5}$$

$$3\sim5$$ 排同理。

#### 答案：

$$f_{N_1,N_2,N_3,N_4,N_5}$$

#### 代码：

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;

const int A = 31;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int k, cn[6];
ll f[A][A][A][A][A];

int main() {
if (k == 0) return 0;
memset(cn, 0, sizeof(cn));
for (int i = 1; i <= k; i++) cn[i] = read();
memset(f, 0, sizeof(f));
f[0][0][0][0][0] = 1;
for (int a = 0; a <= cn[1]; a++)
for (int b = 0; b <= min(a,cn[2]); b++)
for (int c = 0; c <= min(b, cn[3]); c++)
for (int d = 0; d <= min(c, cn[4]); d++)
for (int e = 0; e <= min(d, cn[5]); e++) {
ll &x = f[a][b][c][d][e];
if (a && a - 1 >= b) x += f[a - 1][b][c][d][e];
if (b && b - 1 >= c) x += f[a][b - 1][c][d][e];
if (c && c - 1 >= d) x += f[a][b][c - 1][d][e];
if (d && d - 1 >= e) x += f[a][b][c][d - 1][e];
if (e) x += f[a][b][c][d][e - 1];
}
cout << f[cn[1]][cn[2]][cn[3]][cn[4]][cn[5]] << '\n';
}
return 0;
}


### 【例题】AcWing272 LCIS 最长公共上升子序列

#### 题目分析

$S(i,j+1)=\begin{cases}S(i,j)&A_{i}\le{B_j}\\S(i,j)\bigcup{j}&A_i>{B_j}\end{cases}$

ps：AcWing 上的这道题 $$n=m$$

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 3e3 + 11;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int n, a[A], b[A], f[A][A];

int main() {
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++) b[i] = read();
for (int i = 1; i <= n; i++) {
int val = 0;
if (b[1] < a[i]) val = f[i - 1][0];
for (int j = 2; j <= n; j++) {
if (b[j] == a[i]) f[i][j] = max(f[i][j], val + 1);
else f[i][j] = f[i - 1][j];
if (b[j] < a[i]) val = max(val, f[i - 1][j]);
}
}
int ans = 0;
for (int j = 1; j <= n; j++) ans = max(ans, f[n][j]);
cout << ans << '\n';
}


### 【例题】AcWing273 分级

#### 题目分析

• 如果 $$x>y$$ 则可以令粉色框中的 $$B_i$$ 整体上移直到其中一个 $$B_i$$ 碰到上边界使答案更优。
• 如果 $$x<y$$ 则可以令粉色框中的 $$B_i$$ 整体下移直到其中一个 $$B_i$$ 碰到下边界使答案更优。
• 如果 $$x=y$$ 则上述两种方式均可。

$$f_{i,j}$$ 表示已经排好了 $$B_{1\sim{i}}$$$$B_i=A'_j$$ 的最小花费。

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 2e3 + 11;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int n, m, a[A], b[A], f[A][A], ans = inf;

inline void DP() {
memset(f, 0, sizeof(f));
for (int i = 1; i <= n; i++) {
int minn = inf;
for (int j = 1; j <= m; j++) {
minn = min(minn, f[i - 1][j]);
f[i][j] = minn + abs(a[i] - b[j]);
}
}
for (int i = 1; i <= m; i++) ans = min(ans, f[n][i]);
}

int main() {
for (int i = 1; i <= n; i++) b[i] = a[i] = read();
sort(b + 1, b + 1 + n);
m = unique(b + 1, b + 1 + n) - b - 1;
DP();
reverse(a + 1, a + 1 + n);
DP();
cout << ans << '\n';
return 0;
}


### 【例题】AcWing274 移动服务

• f[i][p[i+1]][y][z]=min(f[i][p[i+1]][y][z],f[i][x][y][z]+c[x][p[i+1]])
• f[i][x][p[i+1]][z]=min(f[i][x][p[i+1]][z],f[i][x][y][z]+c[y][p[i+1]])
• f[i][x][y][p[i+1]]=min(f[i][x][y][p[i+1]],f[i][x][y][z]+c[z][p[i+1]])

${\begin{cases}f_{i+1,x,y}=\min(f_{i+1,x,y},f_{i,x,y}+c_{p_{i},p_{i+1}})\\f_{i+1,p_{i},y}=\min(f_{i+1,p_{i},y},f_{i,x,y}+c_{x,p_{i+1}})\\f_{i+1,x,p_{i}}=\min(f_{i+1,x,p_{i}},f_{i,x,y}+c_{y,p_{i+1}})\end{cases}}$

$$p_{0}=3$$，则可以初始化 $$f_{0,1,2}=0$$，最后的答案就是 $$\min\limits_{1\le{i},{j}\le{L}}f_{n,i,j}$$

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 1010;
const int B = 211;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int l, n, p[A], c[B][B], f[A][B][B];

int main() {
for (int i = 1; i <= l; i++)
for (int j = 1; j <= l; j++) c[i][j] = read();
for (int i = 1; i <= n; i++) p[i] = read();
memset(f, inf, sizeof(f));
f[0][1][2] = 0, p[0] = 3;
for (int i = 0; i < n; i++) {
for (int x = 1; x <= l; x++) {
for (int y = 1; y <= l; y++) {
if (x == y || x == p[i] || y == p[i]) continue;
f[i + 1][x][y] = min(f[i][x][y] + c[p[i]][p[i + 1]], f[i + 1][x][y]);
f[i + 1][p[i]][y] = min(f[i][x][y] + c[x][p[i + 1]], f[i + 1][p[i]][y]);
f[i + 1][x][p[i]] = min(f[i][x][y] + c[y][p[i + 1]], f[i + 1][x][p[i]]);
}
}
}
int ans = inf;
for (int i = 1; i <= l; i++)
for (int j = 1; j <= l; j++) ans = min(ans, f[n][i][j]);
cout << ans << '\n';
return 0;
}


#### 启发

• 求解线性DP问题，一般先确定阶段。若阶段不足以表示一个状态，可以把所需的附加信息也作为状态的维度。
• 若转移时总是从一个阶段转移到下一个阶段，则没有必要关心附加信息维度的大小变化情况，因为无后效性已经由“阶段”保证。
• 在确定DP状态时，要选择最小的能够覆盖整个状态空间的“维度集合”。若DP状态由多个维度构成，则可以思考一下能否由几个维度推出另一个维度，从而降低空间复杂度。

### 【例题】AcWing275 传纸条

$$f_{k, i, j}$$ 表示两个人同时走了 $$k$$ 步，第一个人在 $$(i, k - i)$$ 处，第二个人在 $$(j, k - j)$$ 处的所有走法的最大分值。

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 55;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int n, m, val[A][A], f[A << 1][A][A];

int main() {
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) val[i][j] = read();
for (int k = 2; k <= n + m; k++)
for (int i = max(1, k - m); i <= n && i < k; i++)
for (int j = max(1, k - m); j <= n && j < k; j++)
for (int a = 0; a <= 1; a++)
for (int b = 0; b <= 1; b++) {
int now = val[i][k - i];
if (i != j || k == 2 || k == n + m) {
now += val[j][k - j];
f[k][i][j] = max(f[k][i][j], f[k - 1][i - a][j - b] + now);
}
}
cout << f[n + m][n][n] << '\n';
return 0;
}


### AcWing277 饼干

#### 题目分析

• 如果第 $$i$$ 个小朋友获得的饼干数不为 $$1$$$$j>=i$$，那么 $$f_{i,j}$$ 的一个可行选择为 $$f_{i,j-i}$$，这两个式子是等价的，前 $$i$$ 个小朋友分了 $$j$$ 块饼干等价于前 $$i$$ 个小朋友分了 $$j-i$$ 块饼干，原因是这样相当于每个人少拿一块饼干，但是获得的饼干数量的相对顺序是不变的，所以怨气值之和也是不会变的。
• 如果第 $$i$$ 个小朋友获得的饼干数为 $$1$$，那么就可以枚举前面有多少个小朋友获得的饼干数为 $$1$$，从中取最小值，这一步可以用前缀和优化。

$f_{i,j}=\min\begin{cases}f_{i,j-i}&\text{if } j\ge i\\\min\limits_{k=0}^{i-1}(f_{k,j-(i-k)}+k\times\sum\limits_{x=k+1}^{i}g_x)&\text{if }j\ge(i-k)\end{cases}$

#### 代码

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#define pii pair <int, int>
using namespace std;

const int A = 33;
const int B = 5011;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

pii g[A];
int n, m, f[A][B], sum[A], ans[A];

int main() {
for (int i = 1; i <= n; i++) {
g[i].second = i;
}
sort(g + 1, g + 1 + n);
reverse(g + 1, g + 1 + n);
for (int i = 1; i <= n; i++)
sum[i] = sum[i - 1] + g[i].first;
memset(f, inf, sizeof(f));
f[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (j >= i) f[i][j] = f[i][j - i];
for (int k = 0; k < i && j >= (i - k); k++)
f[i][j] = min(f[i][j], f[k][j - (i - k)] + k * (sum[i] - sum[k]));
}
}
cout << f[n][m] << '\n';
int i = n, j = m, h = 0;
while (i && j) {
if (j >= i && f[i][j] == f[i][j - i]) j -= i, h++;
else {
for (int k = 1; k <= i && k <= j; k++) {
if (f[i][j] == f[i - k][j - k] + (i - k) * (sum[i] - sum[i - k])) {
for (int x = i; x > i - k; x--) ans[g[x].second] = 1 + h;
i -= k, j -= k;
break;
}
}
}
}
for (int i = 1; i <= n; i++) cout << ans[i] << " ";
puts("");
return 0;
}


## 背包DP

### 0/1背包

$$n$$ 件物品和一个容量为 $$M$$ 的背包。第 $$i$$ 件物品的体积是 $$V_i$$，价值是 $$W_i$$。求解将哪些物品装入背包且容量不超过 $$M$$ 可使价值总和最大。

$$f_{i,j}$$表示前 $$i$$ 件物品恰放入一个容量为 $$j$$ 的背包可以获得的最大价值，转移方程为

$f_{i,j}=\max\begin{cases}f_{i-1,j}\\f_{i-1,j-V_i}+W_i&\text{if }{j}\ge{V_i}\end{cases}$

for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (j < v[i]) f[i][j] = f[i - 1][j];
else f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}


int f[2][maxn_M+1];
int now = 0, last = 1;
for (int i = 1; i <= n; i++) {
swap(now, last);
for (int j = 0; j <= m; j++) {
if (j < v[i]) f[now][j] = f[last][j];
else f[now][j] = max(f[last][j], f[last][j - v[i]] + w[i]);
}
}


我是代码



### AcWing278 数字组合

01背包板子题。

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int A = 1e5 + 11;
const int B = 1e6 + 11;
const int mod = 1e9 + 7;
const int inf = 0x3f3f3f3f;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int n, m, f[A], a[A];

int main() {
f[0] = 1;
for (int i = 1; i <= n; i++) a[i] = read();
for (int i = 1; i <= n; i++)
for (int j = m; j >= a[i]; j--) f[j] += f[j - a[i]];
cout << f[m] << "\n";
return 0;
}


### 完全背包

$$n$$ 种物品和一个容量为 $$M$$ 的背包。每种物品都有无限个，第 $$i$$ 种物品的体积是 $$V_i$$，价值是 $$W_i$$。求解将哪些物品装入背包且容量不超过 $$M$$ 可使价值总和最大。

$$f_{i,j}$$表示前 $$i$$ 件物品恰放入一个容量为 $$j$$ 的背包可以获得的最大价值，转移方程为

$f_{i,j}=\max\begin{cases}f_{i-1,j}\\f_{i,j-V_i}+W_i&\text{if }{j}\ge{V_i}\end{cases}$

int f[100010], n, m, v[A], w[A];
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]);
int ans = 0;
for (int i = 0; i <= m; i++) ans = max(ans, f[i]);
cout << ans << '\n';


### AcWing279 自然数拆分

#include <cmath>
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;

const int A = 1e5 + 11;
const int B = 1e6 + 11;
const int mod = 2147483648;

char c = getchar();
int x = 0, f = 1;
for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
return x * f;
}

int n, f[A];

signed main() {
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;
}
}
cout << f[n] << '\n';
return 0;
}


### AcWing280 陪审团

#include <bits/stdc++.h>
#define mem(x) memset(x, 0, sizeof(x))
using namespace std;
const int MAXN = 205;
int drr[MAXN], prr[MAXN], dp[25][805], lujing[25][805][500];
void Init() {
mem(drr), mem(prr), mem(lujing);
for(int i = 0; i < 25; i ++) {
for(int j = 0; j < 805; j ++) {
dp[i][j] = -1;
}
}
dp[0][400] = 0;
}
int n, m;
int main() {
int step = 0;
while(~scanf("%d%d", &n, &m) && (n || m)) {
Init();
for(int i = 1; i <= n; i ++) {
scanf("%d%d", &drr[i], &prr[i]);
}
for(int i = 1; i <= n; i ++) {
for(int j = m; j > 0; j --) {
for(int k = 0; k <= 800; k ++) {
if(k - (drr[i] - prr[i]) >= 0 && dp[j - 1][k - (drr[i] - prr[i])] >= 0 && k - (drr[i] - prr[i]) <= 800) {
if(dp[j - 1][k - (drr[i] - prr[i])] + drr[i] + prr[i] > dp[j][k]) {
dp[j][k] = dp[j - 1][k - (drr[i] - prr[i])] + drr[i] + prr[i];
lujing[j][k][dp[j][k]] = i;
}
}
}
}
}
int sum = 0x7fffffff;
int num = 0;
int re = 0;
int flag = 0;
for(int k = 0; k <= 800; k ++) {
if(k <= 400) {
int temp = 400 - k;
if(temp < sum && dp[m][k] >= 0) {
sum = temp;
num = dp[m][k];
re = k;
flag = 0;
} else if(temp == sum && dp[m][k] >= num) {
num = dp[m][k];
re = k;
flag = 0;
}
} else {
int temp = k - 400;
if(temp < sum && dp[m][k] >= 0) {
sum = temp;
num = dp[m][k];
re = k;
flag = 1;
} else if(temp == sum && dp[m][k] >= num) {
num = dp[m][k];
re = k;
flag = 1;
}
}
}
int a, b;
if(flag == 1) {
a = (sum + num) / 2;
b = num - a;
} else {
a = (num - sum) / 2;
b = num - a;
}
printf("Jury #%d\n", ++ step);
printf("Best jury has value %d for prosecution and value %d for defence:\n", a, b);
vector<int>vec;
vec.clear();
int k = re;
int mysum = dp[m][k];
while(lujing[m][k][mysum]) {
vec.push_back(lujing[m][k][mysum]);
int temp = lujing[m][k][mysum];
m --;
k = k - (drr[temp] - prr[temp]);
mysum = mysum - drr[temp] - prr[temp];
}
sort(vec.begin(), vec.end());
for(int i = 0; i < vec.size(); i ++) {
printf(" %d", vec[i]);
}
printf("\n\n");
}
return 0;
}


### AcWing281 硬币

int used[100010];
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) used[j] = 0;
for (int j = a[i]; j <= m; j++)
if (!f[j] && f[j - a[i]] && used[j - a[i]] < c[i])
f[j] = true, used[j] = used[j - a[i]] + 1;
}


### 分组背包

$$f_{i,j}$$ 表示从前 $$i$$ 组中选出总体积为 $$j$$ 的物品放入背包，物品的最大价值和。

$f_{i,j}=\max\begin{cases}f_{i-1,j}\\\max\limits_{1\le{k}\le{C_i}}(f_{i - 1, j - v_{i,k}}+w_{i,k})\end{cases}$

f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 1; k <= c[i]; k++)
if (j > v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);


## 区间DP

posted @ 2020-11-10 22:17  Loceaner  阅读(120)  评论(0编辑  收藏  举报