目录

  • 0x51.动态规划 - 线性DP
    • 0x51.1 LIS问题
      • Problem A. 登山 (最长下降子序列)
      • Problem B. 友好城市(思维)
      • Problem C. 最大上升子序列和
    • 0x51.2 LCS 问题
      • Problem A. 最长公共上升子序列(数据结构优化)
    • 0x51.3 数字三角形
      • Problem A. 传纸条(降维优化)
    • 0x51.4 线性DP的更多应用
      • Problem A. 杨老师的拍照序列
      • Problem B. 分级(性质优化)
      • Problem C. 移动服务(DP优化)
      • Problem D. I-区域(计算几何)
      • Problem E. 饼干 (根据贪心策略转移)

本系列博客是《算法竞赛进阶指南》的学习笔记,包含书中的部分重要知识点、例题解题报告及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络 ,由我个人整理总结。部分内容由我个人编写而成,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》——作者李煜东,强烈安利,好书不火系列,谢谢配合。
%
学习笔记目录链接: 学习笔记目录链接
%
整理的算法模板合集: ACM模板
%
点我看算法全家桶系列!!!


0x51.动态规划 - 线性DP

线性DP指具有线性 “阶段” 划分的动态规划算法被统称为线性DP。

无论线性DP的状态表示是一维的还是多维的,都是 “线性上的递推 ”。DP的阶段沿着各个维度线性增长,从一个或多个边界点开始有方向地向整个状态空间转移、拓展。最后每个状态上都保留了以自身为 “目标” 的子问题的最优解。

0x51.1 LIS问题

问题描述 :最长上升子序列,给定一个长度为 nnn 的数列 A,求数值单调递增的子序列的长度最长是多少。

状态表示:f[i]f[i]f[i] 表示以 A[i]A[i]A[i] 为结尾的 “最长上升子序列” 的长度

阶段划分:子序列的结尾位置(数列 A 中的位置,从前到后)

转移方程:f[i]=max⁡{f[j]+1∣0≤j<i,A[j]<A[i]}f[i]=\max\{f[j] + 1 \mid 0\le j<i, A[j]<A[i]\}f[i]=max{f[j]+1∣0≤j<i,A[j]<A[i]}

边界: f[0]=0f[0] = 0f[0]=0

目标:max⁡{f[i]∣1≤i≤n}\max\{f[i]\mid 1\le i\le n\}max{f[i]∣1≤i≤n}

时间复杂度 :暴力 O(n2)O(n^2)O(n2), 二分优化 / 数据结构优化 O(nlog⁡n)O(n\log n)O(nlogn)

空间复杂度: O(n)O(n)O(n)

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;int n, m;
int a[N];
int f[N];int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i) scanf("%d", &a[i]);f[0] = 1;for (int i = 1; i <= n; ++ i) {f[i] = 1;for (int j = 1; j < i; ++ j) {if(a[j] < a[i])f[i] = max(f[i], f[j] + 1);}}int ans = 0;for (int i = 1; i <= n; ++ i)ans = max(ans, f[i]);cout << ans << endl;return 0;
}

二分优化

//二分优化,直接维护这个上升序列,把上升序列存到栈里,遇到小的就贪心地把栈里更大的换掉,遇到大的就放到栈里
#include <bits/stdc++.h>using namespace std;const int N = 1e5 + 6, INF = 0x3f3f3f3f;
int n, m;
int a[N];
int f[N], cnt;int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i)scanf("%d", &a[i]);int ans = 0;f[0] = -INF;for (int i = 1; i <= n; ++ i) {if(a[i] > f[cnt]) f[ ++ cnt] = a[i];else *lower_bound(f + 1, f + 1 + cnt, a[i]) = a[i];}cout << cnt << endl;
}

树状数组优化

树状数组 x=aix=a_ix=ai​,维护 y=max⁡{f[i]}y=\max\{f[i]\}y=max{f[i]}

这样我们在更新的时候就可以 O(log⁡n)O(\log n)O(logn) 查询小于 a[i]a[i]a[i] 的 a[j]a[j]a[j] 里最大的 f[j]f[j]f[j] 直接更新即可。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6 + 6;
#define lowbit(x) (x & (-x))
int n, m, s, t;
int ans;
int maxx;
int tr[maxn];
int f[maxn];
int a[maxn];
int b[maxn];void update(int x, int k)
{for (;x <= maxn; x += lowbit(x)) {tr[x] = max(tr[x], k);}
}int query(int x)
{int res = 0;for (;x; x -= lowbit(x)) {res = max(res, tr[x]);}return res;
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i) {scanf("%d", &a[i]);b[i] = a[i]; } sort(b + 1, b + 1 + n);int m = unique(b + 1, b + 1 + n) - b - 1;      for (int i = 1; i <= n; ++ i)a[i] = lower_bound(b + 1, b + 1 + m, a[i]) - b; for (int i = 1; i <= n; ++ i) { f[i] = query(a[i] - 1) + 1;update(a[i], f[i]);ans = max(ans, f[i]);}cout << ans << endl;return 0;
}

Problem A. 登山 (最长下降子序列)

AcWing 1014

五一到了,ACM队组织大家去登山观光,队员们发现山上一共有 NNN 个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。

同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。

队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

2≤N≤10002≤N≤10002≤N≤1000

Solution

本题实际上是求一个最大上升子序列到一个点,然后再以该点为起点求一个最长下降子序列。所以我们可以先预处理出正向的最长上升子序列,再求一个逆向的最长上升子序列(就是最长下降子序列),然后枚举每一个点作为起点,求一个最大值。要注意的是枚举的这个起点被两个子序列算了两次,所以要 -1

Code

#include<iostream>
#include<cstdio>
#include<algorithm>using namespace std;const int N = 1010, M = 50007, INF = 0x3f3f3f3f;int n, m;
int t;
int a[N], f[N], g[N];int main(){scanf("%d",&n);for(int i = 1;i <= n;++i)scanf("%d",&a[i]);for(int i = 1;i <= n;++ i){f[i] = 1;for(int j = 1;j < i;++j)if(a[j] < a[i])f[i] = max(f[i], f[j] + 1);}for(int i = n;i >= 1;-- i){g[i] = 1;for(int j = n;j > i;--j)if(a[j] < a[i])g[i] = max(g[i],g[j] + 1); }int res = 0;for(int i = 1;i <= n;++i)res = max(res, f[i] + g[i] - 1);printf("%d\n",res);return 0;
}

Problem B. 友好城市(思维)

AcWing 1012

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的 NNN 个城市。

北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。

每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。

编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

1≤N≤5000,0≤xi≤100001≤N≤5000,0≤x_i≤100001≤N≤5000,0≤xi​≤10000

Solution

当i>ji>ji>j且i友>j友i_{友}>j_友i友​>j友​时不交叉

所以可以将南岸从小到大排序 求北岸的最长上升子序列

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>using namespace std;const int N = 5000007, M = 5000007, INF = 0x3f3f3f3f;
typedef pair<int,int> PII;
int n, m;
int b[N];
int f[N];
PII a[N];
int len = 1;
int main(){scanf("%d",&n);for(int i = 1;i <= n;++i){scanf("%d%d",&a[i].first, &a[i].second);}sort(a + 1,a + 1 + n);f[1] = a[1].second;for(int i = 2;i <= n;++i){if(f[len] < a[i].second)f[++len] = a[i].second;else {//int tmp = lower_bound(f + 1, f + 1 + len, a[i].second) - f;//f[tmp] = a[i].second;*lower_bound(f + 1 ,f + 1 + len,a[i].second) = a[i].second;}}printf("%d\n",len);return 0;
}

Problem C. 最大上升子序列和

AcWing 1016

一个数的序列 bib_ibi​,当 b1<b2<…<bSb_1<b_2<…<b_Sb1​<b2​<…<bS​ 的时候,我们称这个序列是上升的。

对于给定的一个序列 (a1,a2,…,aN)(a_1,a_2,…,a_N)(a1​,a2​,…,aN​) ,我们可以得到一些上升的子序列 (ai1,ai2,…,aiK)(a_{i_1},a_{i_2},…,a_{i_K})(ai1​​,ai2​​,…,aiK​​) ,这里 1≤i1<i2<…<iK≤N1≤i_1<i_2<…<i_K≤N1≤i1​<i2​<…<iK​≤N 。

比如,对于序列 (1,7,3,5,9,4,8)(1,7,3,5,9,4,8)(1,7,3,5,9,4,8) ,有它的一些上升子序列,如 (1,7),(3,4,8)(1,7),(3,4,8)(1,7),(3,4,8) 等等。

这些子序列中和最大为 181818 ,为子序列 (1,3,5,9)(1,3,5,9)(1,3,5,9) 的和。

你的任务,就是对于给定的序列,求出最大上升子序列和。

注意,最长的上升子序列的和不一定是最大的,比如序列 (100,1,2,3)(100,1,2,3)(100,1,2,3) 的最大上升子序列和为 100100100 ,而最长上升子序列为 (1,2,3)(1,2,3)(1,2,3)。

1≤N≤10001≤N≤10001≤N≤1000

Solution

初始化:f[i] = a[i]

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 5007, INF = 0x3f3f3f3f;int n, m;
int a[N], f[N];int main(){scanf("%d",&n);for(int i = 1;i <= n;++i)scanf("%d",&a[i]);for(int i = 1;i <= n;++i){f[i] = a[i];for(int j = 1;j < i;++j)if(a[j] < a[i])f[i] = max(f[i],f[j] + a[i]);}int res = 0;for(int i = 1;i <= n;++i)res = max(res,f[i]);printf("%d\n",res);return 0;}

0x51.2 LCS 问题

问题描述:最长公共子序列,给定两个长度分别为 n 和 m 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

状态表示f[i, j] 表示 A 的前缀子串与 B 的前缀子串的 “最长公共子序列” 的长度

阶段划分:已经处理过的前缀长度(两个字符串中的位置,即一个二维坐标)

转移方程
A[i] != B[j], f[i,j]=max⁡{f[i−1,j],f[i,j−1]}f[i,j]=\max\{f[i-1,j],f[i,j-1]\}f[i,j]=max{f[i−1,j],f[i,j−1]}
A[i] == B[j], f[i,j]=max⁡{f[i−1,j−1]+1}f[i,j] = \max\{f[i-1, j-1]+1\}f[i,j]=max{f[i−1,j−1]+1}

边界: f[i,0]=f[0,j]=0f[i,0] = f[0,j] = 0f[i,0]=f[0,j]=0

目标:f[n,m]f[n,m]f[n,m]

时间复杂度

暴力 O(n2)O(n^2)O(n2)

优化 —— 转为 LIS, O(nlog⁡n)O(n\log n)O(nlogn)

Code

#include <bits/stdc++.h>using namespace std;const int N = 2e3 + 7;
int n, m;
char a[N], b[N];
int f[N][N];int main()
{scanf("%d%d", &n, &m);scanf("%s%s", 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] = max(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;
}

Problem A. 最长公共上升子序列(数据结构优化)

AcWing 272

Problem

给定两个数列 A 和 B,求两序列的最长公共上升子序列的长度。对于两个数列 A 和 B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列。

Solution

我们知道最长上升子序列的阶段为 f[i]f[i]f[i] 表示以 a[i]a[i]a[i] 为结尾的最长上升子序列的长度,最长公共子序列的阶段为 f[i,j]f[i,j]f[i,j] 表示 a[1∼i]a[1\sim i]a[1∼i]和 b[1∼j]b[1\sim j]b[1∼j]的最长公共子序列的长度。因此我们设 f[i,j]f[i, j]f[i,j] 表示序列 A[1...i],B[1...j]A[1...i],B[1...j]A[1...i],B[1...j] 里,以 BjB_jBj​ 为结尾的最长公共上升子序列的长度。

暴力转移显然是 O(n3)O(n^3)O(n3):

for (int i = 1; i <= n; i++) f[i][0]=0;
b[0] = -(1<<30);
for (int i = 1; i <= n; i++)for (int j = 1; j <= m; j++) {f[i][j] = f[i-1][j];if (a[i]==b[j]) {for (int k = 0; k < j; k++)if (b[k] < a[i]) // b[j] == a[i]f[i][j] = max(f[i][j], f[i-1][k]+1);}}
int ans = 0;
for (int j = 1; j <= m; j++)ans = max(ans, f[n][j]);

考虑优化。

发现每次第三重循环的时候只新增了一个变量进入决策,也即对于每一个 fori←1ton\mathrm{for}\ i \leftarrow 1\ \mathrm{to} \ nfor i←1 to n 来说,每次维护的决策集合 max⁡{b[k]<a[i]}\max\{b[k]<a[i]\}max{b[k]<a[i]} 是完全相同的,随着 forj←1tom\mathrm{for}\ j \leftarrow 1\ \mathrm{to} \ mfor j←1 to m每次仅增加一个新变量,即决策集合中的元素只增多不减少,因此我们就可以使用数据结构维护,我们这里只需要用一个整型变量 maxx 维护 max⁡{}\max\{\}max{} 即可。因此我们需要维护一个集合 SSS,支持把一个新的决策 j−1j -1j−1插入集合 SSS,求集合 SSS 中下标对应的 f[i−1][k]+1f[i-1][k]+1f[i−1][k]+1 值的 max⁡\maxmax。

时间复杂度 :O(n2)O(n^2)O(n2)

Code

#include <bits/stdc++.h>using namespace std;const int N = 5e3 + 7;int n, m, s, t, k;
int f[N][N];
int a[N], b[N];int main()
{scanf("%d", &n);m = n;for (int i = 1; i <= n; ++ i)scanf("%d", &a[i]);for (int i = 1; i <= m; ++ i)scanf("%d", &b[i]);for (int i = 1; i <= n; ++ i) {int maxx = 1;for (int j = 1; j <= m; ++ j) {if(a[i] == b[j]) f[i][j] = max(f[i][j], maxx);else f[i][j] = f[i - 1][j];if(b[j] < a[i]) maxx = max(maxx, f[i - 1][j] + 1);}}int ans = 0;for (int i = 1; i <= m; ++ i) ans = max(ans, f[n][i]);cout << ans << endl;return 0;
}

0x51.3 数字三角形

问题描述:给定一个共有 nnn 行的三角矩阵 A,其中第 iii 行有 jjj 列,从左上角出发,每次可以向下方或者右下方走一步,最终到达地步,求把经过的所有位置上的数加起来的最大和是多少。

状态表示f[i,j] 表示从左上角走到第 iii 行第 jjj 列,和最大是多少

阶段划分: 路径的结尾位置(矩阵的行列位置,即一个二维坐标)

转移方程

j == i,f[i,j]=A[i,j]+f[i−1,j−1]f[i,j] = A[i,j]+f[i-1,j-1]f[i,j]=A[i,j]+f[i−1,j−1]
j == 1,f[i,j]=A[i,j]+f[i−1,j]f[i,j] = A[i,j]+f[i-1,j]f[i,j]=A[i,j]+f[i−1,j]
j > 1, f[i,j]=A[i,j]+max⁡{f[i−1,j−1],f[i−1,j]}f[i,j] = A[i,j]+\max\{f[i-1,j-1], f[i - 1, j]\}f[i,j]=A[i,j]+max{f[i−1,j−1],f[i−1,j]}

边界:f[1][1]=A[1][1]f[1][1] = A[1][1]f[1][1]=A[1][1]

目标: max⁡{f[n,i]∣1≤j≤n}\max\{f[n,i]\mid 1\le j\le n\}max{f[n,i]∣1≤j≤n}

时间复杂度:O(n2)O(n^2)O(n2)

Code

#include <bits/stdc++.h>using namespace std;
const int N = 2e3 + 6, INF = 0x3f3f3f3f;int n, m, s, t, k, ans = -INF, a[N][N], f[N][N];int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i) for (int j = 1; j <= i; ++ j) scanf("%d", &a[i][j]); f[1][1] = a[1][1];for (int i = 2; i <= n; ++ i) {for (int j = 1; j <= i; ++ j) {if(j == 1) f[i][j] = a[i][j] + f[i - 1][j];else if(j == i) f[i][j] = a[i][j] + f[i - 1][j - 1];else f[i][j] = a[i][j] + max(f[i - 1][j], f[i - 1][j - 1]);}} for (int i = 1; i <= n; ++ i)ans = max(ans, f[n][i]);cout << ans << endl;return 0;
}

Problem A. 传纸条(降维优化)

AcWing 275

Problem

给定一个 N×MN\times MN×M 的矩阵 AAA,每个格子中有一个整数,现在需要找到两条从左上角 (1,1)(1, 1)(1,1) 到右下角 (N,M)(N,M)(N,M) 的路径,路径上的每一步只能向右或者向下走。路径经过的格子中的数会被取走,若两条路径同时经过一个格子,只算一次,求取得的数之和最大是多少 N,M≤50N,M\le 50N,M≤50

Solution

考虑DP的阶段,由于每一个格子只能取一次,所以我们需要能确定两条路中每一步都走到了哪里,我们希望能找到一个线性的阶段转移,显然我们可以考虑两条路同时行走,将两条路径走过的步数作为阶段进行转移。并且我们可以发现如果两条路同时走的话,两条路经过同一个格子,只有同时到达该格子的情况,没有一个先到,另一个后到的情况出现,这样我们讨论起来也比较方便。为了确定每一步的位置,我们还需要知道这两条路径的每一步都走到了哪里设:(x1,y1),(x2,y2)(x_1,y_1), (x_2, y_2)(x1​,y1​),(x2​,y2​),即 f[i][x1][y1][x2][y2]f[i][x_1][y_1][x_2][y_2]f[i][x1​][y1​][x2​][y2​] 表示两条路径同时走了 iii 步,一条路径走到了 (x1,y1)(x_1, y_1)(x1​,y1​),一条路径走到了 (x2,y2)(x_2,y_2)(x2​,y2​) 的最大路径之和。

这样直接转移复杂度显然是 O(n5)O(n^5)O(n5) 的复杂度,考虑优化。

我们希望能减少枚举的维度,所以考虑当前的五维之间有没有什么联系。由于两条路是同时行走,显然有

x1+y1=x2+y2=i+2x_1+y_1=x_2+y_2=i+2 x1​+y1​=x2​+y2​=i+2

因此我们只需要枚举三维:f[i][x1][x2],1≤x1,x2≤min⁡{n,i+1}f[i][x_1][x_2],1\le x_1,x_2\le \min\{n, i + 1\}f[i][x1​][x2​],1≤x1​,x2​≤min{n,i+1} ,显然 y1=i+2−x1,y2=i+2−x2y_1=i+2-x_1,y_2=i+2-x_2y1​=i+2−x1​,y2​=i+2−x2​ 可以直接推导得到。

然后分别考虑两条路径转移,有向下和向右两种转移方法,一共同时向右、同时向下、1向右2向下和1向下2向右四种情况。

分别讨论这四种情况当中走到同一个格子和没有走到同一个格子即可。

时间复杂度 O(n3)O(n^3)O(n3)

Code

#include <cstdio>using namespace std;const int N = 60;int min(int a, int b)
{return a < b ? a : b;
}int max(int a, int b)
{return a > b ? a : b;
}int n, m, s, t, k, ans;
int a[N][N];
int f[507][N][N];int main()
{scanf("%d%d", &n, &m);for (int i = 1; i <= n; ++ i)for (int j = 1; j <= m; ++ j)scanf("%d", &a[i][j]);f[0][1][1] = a[1][1];for (int i = 0; i < n + m - 2; ++ i) {for (int x1 = 1; x1 <= min(n, i + 1); ++ x1) {for (int x2 = 1; x2 <= min(n, i + 1); ++ x2) {if(x1 == x2) {int y1 = i + 2 - x1, y2 = i + 2 - x2;f[i + 1][x1][x2] = max(f[i + 1][x1][x2], f[i][x1][x2] + a[x1][i + 1 + 2 - x1]);f[i + 1][x1 + 1][x2 + 1] = max(f[i + 1][x1 + 1][x2 + 1], f[i][x1][x2] + a[x1 + 1][y1]);}else {int y1 = (i + 1) + 2 - x1, y2 = (i + 1) + 2 - x2;f[i + 1][x1][x2] = max(f[i + 1][x1][x2], f[i][x1][x2] + a[x1][y1] + a[x2][y2]);y1 = i + 2 - x1, y2 = i + 2 - x2,f[i + 1][x1 + 1][x2 + 1] = max(f[i + 1][x1 + 1][x2 + 1], f[i][x1][x2] + a[x1 + 1][y1] + a[x2 + 1][y2]);}if(x1 + 1 == x2) f[i + 1][x1 + 1][x2] = max(f[i + 1][x1 + 1][x2], f[i][x1][x2] + a[x2][i + 1 + 2 - x2]);else f[i + 1][x1 + 1][x2] = max(f[i + 1][x1 + 1][x2], f[i][x1][x2] + a[x1 + 1][i + 1 + 2 - x1 - 1] + a[x2][i + 1 + 2 - x2]);if(x2 + 1 == x1)f[i + 1][x1][x2 + 1] = max(f[i + 1][x1][x2 + 1], f[i][x1][x2] + a[x1][i + 1 + 2 - x1]);else f[i + 1][x1][x2 + 1] = max(f[i + 1][x1][x2 + 1], f[i][x1][x2] + a[x1][i + 1 + 2 - x1] + a[x2 + 1][i + 1 + 2 - x2 - 1]);}}}printf("%d\n", f[n + m - 2][n][n]);return 0;
}

0x51.4 线性DP的更多应用

Problem A. 杨老师的拍照序列

AcWing 271

Problem

有 nnn 个同学,占成左对齐的 kkk 排,第 iii 排占 NiN_iNi​ 个人,第 111 排在最后面,第 kkk 排在最前面,每个人的身高均不同,身高分别为 1,2⋯n1,2\cdots n1,2⋯n,在合影时要求每一排从左到右身高递增,每一列从后到前身高递增,问一共有多少种合影位置方案。n≤30,k≤5n\le 30,k\le 5n≤30,k≤5

Solution

我们在设计状态的时候,由于不同的人的安排不同,会出现很多杂乱的结果,而DP的核心就是从集合的角度出发设计状态,使用一个集合表示一种状态,避免讨论具体杂乱的方案。根据题意,由于由于要求每行每列都是单调递增,为了简化问题,我们在放置人的时候按照身高从小到大的顺序放置每一个人。

对于每行的放置情况,由于我们是从小到大的顺序放置的,因此每行的人的放置顺序一定是从左到右连续放置,因为如果当前位置未连续放置,有空缺,那么后面再放到这个空缺的时候,后放置的人一定比之前的大,即这个人比左右的人都大,不符合从左到右单调递增的要求。

对于每列的放置情况,从后往前的每排的人数一定单调递减,否则当前行第 iii 排放置的人比后面第 i−1i - 1i−1 排的人多,那么再放置第 i−1i - 1i−1 排的时候,当前放置的位置在第 iii 排已经放置过了,且当前放置的人一定比之前放置的第 iii 排人大,不满足从后往前单调递增的性质。

状态表示:设 f[a][b][c][d][e] 代表从后往前每排人数分别为 a,b,c,d,ea, b, c, d, ea,b,c,d,e 的所有方案的集合,f[a][b][c][d][e] 的值是该集合中元素的数量
其中要求 a≥b≥c≥d≥ea\ge b \ge c \ge d \ge ea≥b≥c≥d≥e

阶段划分:每排放置人数为 a,b,c,d,ea, b, c, d, ea,b,c,d,e 时的所有方案的集合

转移方程: 当 a > 0 && a > b 时,最后一个同学可能被安排在第 111 排,方案数 f[a + 1][b][c][d][e] += f[a][b][c][d][e],b,c,d,eb, c, d, eb,c,d,e 同理

边界:f[0,0,0,0,0]=1f[0, 0, 0, 0, 0] = 1f[0,0,0,0,0]=1

目标:f[N1,N2,N3,N4,N5]f[N_1, N_2, N_3, N_4,N_5]f[N1​,N2​,N3​,N4​,N5​]

复杂度:O(k×(nk)k)O(k\times {(\dfrac {n}{k})}^k)O(k×(kn​)k)

Code

#include <bits/stdc++.h>using namespace std;
using ll = long long;
const int N = 32;int n, m, k;
ll f[N][N][N][N][N];int main()
{while(scanf("%d", &k), k) { int s[6] = {0};for (int i = 1; i <= k; ++ i)  scanf("%d", &s[i]);  memset(f, 0, sizeof f);f[0][0][0][0][0] = 1;for (int a = 0; a <= s[1]; ++ a) for (int b = 0; b <= min(a, s[2]); ++ b) for (int c = 0; c <= min(b, s[3]); ++ c)for (int d = 0; d <= min(c, s[4]); ++ d)for (int e = 0; e <= min(d, s[5]); ++ e) {ll& res = f[a][b][c][d][e];if(a && a > b) res += f[a - 1][b][c][d][e];if(b && b > c) res += f[a][b - 1][c][d][e];if(c && c > d) res += f[a][b][c - 1][d][e];if(d && d > e) res += f[a][b][c][d - 1][e];if(e) res += f[a][b][c][d][e - 1];}  cout << f[s[1]][s[2]][s[3]][s[4]][s[5]] << endl;}return 0;
}

Problem B. 分级(性质优化)

AcWing 273

Problem

给定长度为 NNN 的序列 A,构造一个长度为 NNN 的序列 B,满足:

  • B 非严格单调,即 B1≤B2≤…≤BNB_1≤B_2≤…≤B_NB1​≤B2​≤…≤BN​ 或 B1≥B2≥…≥BNB_1≥B_2≥…≥B_NB1​≥B2​≥…≥BN​。

  • 最小化 S=∑i=1N∣Ai−Bi∣\displaystyle S=\sum^{N}_{i=1}|A_i−B_i|S=i=1∑N​∣Ai​−Bi​∣。

只需要求出这个最小值 SSS。

1≤N≤2000,0≤Ai≤1091≤N≤2000,0≤A_i≤10^91≤N≤2000,0≤Ai​≤109

Solution

两种情况,先考虑单调递增的情况如何构造。

f[i, j] 表示 A 的前 iii 个数变成非严格单调递增,其中结尾变为了数值 jjj,此时的最小代价。

显然有转移方程 f[i,j]=min⁡{f[i−1,k]∣k≤j}+abs(a[i]−j)f[i, j] = \min\{f[i-1,k] \mid k \le j\} + \text{abs}(a[i] - j)f[i,j]=min{f[i−1,k]∣k≤j}+abs(a[i]−j)

发现我们在取 min⁡{f[i−1,k]∣k≤j}\min\{f[i-1,k] \mid k \le j\}min{f[i−1,k]∣k≤j} 的时候,该集合内的元素只增不减,即决策集合中的元素只增多不减少,因此我们就可以使用数据结构维护,仅用一个变量 minn 维护 min⁡{}\min\{\}min{} 即可。

每次的 minn 在使用之后更新一下, += abs(a[i] - j)
即可 O(1)O(1)O(1) 维护决策集合,枚举 k≤j,O(109)k \le j,O(10^9)k≤j,O(109),总复杂度:2000×1092000 \times 10^92000×109,考虑优化。

引理: 在满足最小化 S=∑i=1N∣Ai−Bi∣\displaystyle S=\sum^{N}_{i=1}|A_i−B_i|S=i=1∑N​∣Ai​−Bi​∣ 的前提下,一定存在一种构造序列 B 的方案,使得 B 中的数值都在 A 中出现过。

动态规划本身就是一个按照阶段推导的方法,因此猜想证明也可以额按照阶段证明,即数学归纳法证明即可。具体利用中位数的性质画图即可证明。

引理证明后,显然我们可以将序列 A 离散化,仅使用 A 里的数来构造 B 即可。

总时间复杂度:2000×109⇒2000×2000=N22000 \times 10^9 \Rightarrow 2000 \times 2000 = N^22000×109⇒2000×2000=N2

最后考虑如何求解单调递减的情况。
原来的 num 是递增的,小的向大的转移,即 B 序列单调递增
现在还需要求 B 序列单调递减,我们可以将 num 反转,num 变成递减的,大的向小的转移,即 B 序列单调。

Code

#include <bits/stdc++.h>using namespace std;
using ll = long long;
const int N = 2e3 + 7, INF = 0x3f3f3f3f;int n, m, s, t;
int a[N];
int b[N], id[N];
ll num[N];
ll f[N][N];
ll res;ll solve()
{memset(f, 0x3f, sizeof f);f[0][0] = 0;for (int i = 1; i <= n; ++ i) {ll minn = f[i - 1][0];for (int j = 1; j <= m; ++ j) {minn = min(minn, f[i - 1][j]);f[i][j] = minn + abs(a[i] - num[j]);}}ll ans = INF;for (int i = 1; i <= m; ++ i)ans = min(ans, f[n][i]);return ans;
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++ i) {scanf("%d", &a[i]);num[i] = a[i];}sort(num + 1, num + 1 + n);m = unique(num + 1, num + 1 + n) - num - 1;for (int i = 1; i <= n; ++ i) id[i] = lower_bound(num + 1, num + 1 + m, a[i]) - num;res = solve(); reverse(num + 1, num + 1 + m); res = min(res, solve());cout << res << endl;return 0;
}

Problem C. 移动服务(DP优化)

AcWing 274

Problem

一个公司有三个移动服务员,最初分别在位置 111,222,333 处。

如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。

某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工(可穿过)。

从 ppp 到 qqq 移动一个员工,需要花费 c(p,q)c(p,q)c(p,q)。

这个函数不一定对称,但保证 c(p,p)=0c(p,p)=0c(p,p)=0。

给出 NNN 个请求,请求发生的位置分别为 p1∼pNp_1\sim p_Np1​∼pN​。

公司必须按顺序依次满足所有请求,且过程中不能去其他额外的位置,目标是最小化公司花费,请你帮忙计算这个最小花费。

Solution

显然 DP 的阶段是当前完成的请求数量

f[i] 表示当前完成了 iii 个请求的最小花费

根据题意,为了计算每次指派服务员的花费,我们需要知道每个服务员的位置

因此设 f[i][x][y][z] 表示当前完成了 iii 个请求,三个服务员分别位于 x,y,zx,y,zx,y,z 位置的最小花费

此时直接转移时间复杂度是 O(n×L3),n=1000,L=200O(n\times L^3),n=1000,L=200O(n×L3),n=1000,L=200,无法接受,考虑优化

由于需要知道每个服务员的位置,但是当我们完成第 iii 个任务时,显然完成该任务的服务员的位置
一定在 pip_ipi​ 的位置,因此我们就可以仅维护两个人服务员的位置,另一个服务员的位置一定在 pip_ipi​

即设 f[i][x][y] 表示当前完成了 iii 个请求,三个服务员分别位于 pi,x,yp_i,x,ypi​,x,y 位置的最小花费
转移方程有

f[i][x][y]=min⁡{f[i−1][x][y]+c[p[i−1]][p[i]]}f[i][x][y] = \min\{f[i - 1][x][y] + c[p[i - 1]][p[i]]\} f[i][x][y]=min{f[i−1][x][y]+c[p[i−1]][p[i]]}
f[i][x][p[i−1]]=min⁡{f[i−1][x][y]+c[y][p[i]]}f[i][x][p[i - 1]] = \min\{f[i - 1][x][y] + c[y][p[i]]\} f[i][x][p[i−1]]=min{f[i−1][x][y]+c[y][p[i]]}
f[i][p[i−1]][y]=min⁡{f[i−1][x][y]+c[x][p[i]]}f[i][p[i - 1]][y] = \min\{f[i - 1][x][y] + c[x][p[i]]\} f[i][p[i−1]][y]=min{f[i−1][x][y]+c[x][p[i]]}

由于三个服务员的初始位置为 1,2,31, 2, 31,2,3 ,故我们可以初始化 p0=3p_0 = 3p0​=3,f[0][1][2]=0f[0][1][2] = 0f[0][1][2]=0 即可。

时间复杂度为 O(n×L2)O(n\times L^2)O(n×L2)

最后需要用滚动数组优化空间,显然我们当前的 f[i][x][y] 仅与 f[i - 1] 有关,所以我们只需要开 f[2] 来保存 f[i & 1]f[i - 1 & 1] 即可。

空间复杂度 O(L2)O(L^2)O(L2)​

Code

#include <bits/stdc++.h>using namespace std;
const int maxL = 207, maxn = 1007, INF = 0x3f3f3f3f;int n, m, s, t, k, ans, L;
int a[maxn];
int p[maxn];
int f[2][maxL][maxL];
int c[maxL][maxL];int main()
{scanf("%d%d", &L, &n);for (int i = 1; i <= L; ++ i) for (int j = 1; j <= L; ++ j)  scanf("%d", &c[i][j]); p[0] = 3;for (int i = 1; i <= n; ++ i) {scanf("%d", &p[i]);}memset(f, 0x3f, sizeof f);f[0][1][2] = 0;for (int i = 1; i <= n; ++ i) {for (int x = 1; x <= L; ++ x) {for (int y = 1; y <= L; ++ y) {if(f[i - 1 & 1][x][y] != INF) {int z = p[i - 1];if(x != p[i] && y != p[i])f[i & 1][x][y] = min(f[i & 1][x][y], f[i - 1 & 1][x][y] + c[z][p[i]]);if(x != p[i] && z != p[i])f[i & 1][x][z] = min(f[i & 1][x][z], f[i - 1 & 1][x][y] + c[y][p[i]]);if(y != p[i] && z != p[i])f[i & 1][z][y] = min(f[i & 1][z][y], f[i - 1 & 1][x][y] + c[x][p[i]]);f[i - 1 & 1][x][y] = INF;}}}}ans = INF;for (int x = 1; x <= L; ++ x) for (int y = 1; y <= L; ++ y) ans = min(ans, f[n & 1][x][y]);cout << ans << endl;return 0;
}

Problem D. I-区域(计算几何)

AcWing 276

Problem

在 N×MN×MN×M 的矩阵中,每个格子有一个权值,要求寻找一个包含 KKK 个格子的凸连通块(连通块中间没有空缺,并且轮廓是凸的),使这个连通块中的格子的权值和最大。

注意:凸连通块是指:连续的若干行,每行的左端点列号先递减、后递增,右端点列号先递增、后递减。

求出这个最大的权值和,并给出连通块的具体方案,输出任意一种方案即可。

1≤N,M≤15,0≤K≤N×M1≤N,M≤15 ,0≤K≤N×M1≤N,M≤15,0≤K≤N×M

Solution

Problem E. 饼干 (根据贪心策略转移)

AcWing 277

Problem

圣诞老人共有 MMM 个饼干,准备全部分给 NNN 个孩子。

每个孩子有一个贪婪度,第 iii 个孩子的贪婪度为 g[i]g[i]g[i]。

如果有 a[i]a[i]a[i] 个孩子拿到的饼干数比第 iii 个孩子多,那么第 iii 个孩子会产生 g[i]×a[i]g[i] \times a[i]g[i]×a[i] 的怨气。

给定 NNN、MMM 和序列 ggg,圣诞老人请你帮他安排一种分配方式,使得每个孩子至少分到一块饼干,并且所有孩子的怨气总和最小。

N≤30,M≤5000N\le 30, M \le 5000N≤30,M≤5000

Solution

线性DP,考虑阶段一个是维度保证走向,然后再存相关的信息,将阶段设为 f[i][j]f[i][j]f[i][j] 表示前 iii 个孩子分配了 jjj 个饼干的最小怒气值。显然直接转移的时候,每个孩子怒气值计算时的 a[i]a[i]a[i] 是会随着饼干的发放情况而发生变化的,不能保证拓扑序无后效性。

考虑找到一种拓扑序递推。DP无思路,可以先尝试贪心。

怒气值等于贪婪度乘上比他分的饼干多的人数:a[i]×g[i]a[i] \times g[i]a[i]×g[i],由于 a[i]a[i]a[i] 动态变化,我们考虑 g[i]g[i]g[i]。发现显然 g[i]g[i]g[i] 越大的孩子应该分配更多的饼干,才会保证怒气值 a[i]×g[i]a[i] \times g[i]a[i]×g[i] 更小。

考虑证明:

假设一种孩子的排列:1,2,3...i,i+1,...,n1, 2, 3 ... i, i + 1, ..., n1,2,3...i,i+1,...,n ,其中 g[i]<g[i+1]g[i] < g[i + 1]g[i]<g[i+1],我们从小到大分配非严格单调递减的饼干数。

我们仅考虑第 iii 个孩子和第 i+1i + 1i+1 个孩子对于答案的贡献。 显然 a[i]=0,a[i+1]=1a[i] = 0, a[i + 1] = 1a[i]=0,a[i+1]=1, 对答案的贡献为 g[i+1]×1g[i+1] \times 1g[i+1]×1

我们进行邻项交换,把 iii 和 i+1i+1i+1 分配的饼干数量交换,即:1,2,3,...,i+1,i,n1, 2, 3, ..., i + 1, i, n1,2,3,...,i+1,i,n。显然 a[i+1]=0,a[i]=1a[i + 1] = 0, a[i] = 1a[i+1]=0,a[i]=1, 对答案的贡献为 g[i]×1g[i] \times 1g[i]×1

而我们这里规定 g[i]<g[i+1]g[i] < g[i + 1]g[i]<g[i+1] ,因此交换后更优,所以我们应该按照 g[i]g[i]g[i] 从大到小的顺序分配非严格单调递减的饼干数最优。

现在可以开始利用贪心策略转移。

设第 iii 个孩子分配 num[i]num[i]num[i] 个饼干。

  • 若 num[i]>num[i+1]num[i] > num[i +1]num[i]>num[i+1],有 a[i+1]=ia[i + 1] = ia[i+1]=i。

  • 若 num[i]=num[i+1]num[i] = num[i +1]num[i]=num[i+1],a[i+1]a[i + 1]a[i+1] 等于 iii 减去与 i+1i + 1i+1 饼干数相等的孩子数。

我们看上去需要维护每个孩子具体得到的饼干数,较难维护。我们发现答案至于饼干数的相对关系有关,因此,对于 f[i][j]f[i][j]f[i][j],前 iii 个孩子分配 jjj 个饼干,由于题目要求每个孩子至少有一个饼干,所以 j≥ij \ge ij≥i。显然前 iii 个孩子每个人少一个饼干,相对顺序不变,即 f[i][j]=f[i][j−i]f[i][j] = f[i][j - i]f[i][j]=f[i][j−i]。

因此我们就可以将每个人的饼干数 num[i]num[i]num[i] 从 num[i]−min⁡{num[i]}num[i] - \min\{num[i]\}num[i]−min{num[i]},即我们将共同拥有的饼干全部减去,直到每人至少一个饼干的条件即将被撼动位置。此时饼干最少的孩子们均只分配了 111 个饼干。此时我们只需要直接枚举找前 iii 个孩子里有多少个同样也只获得了一个饼干即可,即 前 iii 个孩子中的 后 kkk 个孩子一人一个饼干,前 i−ki-ki−k 个孩子分配 j−(i−k)j-(i-k)j−(i−k) 个饼干。我们再从这种情况开始通过 f[i][j]=f[i][j−i]f[i][j] = f[i][j - i]f[i][j]=f[i][j−i] 转移到 f[n][m]f[n][m]f[n][m] 即可。

有转移方程:

f[i][j]=min⁡{f[i][j−i],min⁡0≤k<if[k][j−(i−k)+k×∑p=k+1ig[p]}f[i][j] = \min\{ f[i][j - i],\ \min_{0\le k<i} f[k][j - (i - k) + k \times \sum_{p=k+1}^ig[p] \} f[i][j]=min{f[i][j−i], 0≤k<imin​f[k][j−(i−k)+k×p=k+1∑i​g[p]}

最后输出方案,我们只需要记录当前前 iii 个孩子分配 jjj 个饼干中,有多少个是分配了 111 个饼干,递归输出饼干分配方案即可。

时间复杂度 O(n2m)O(n^2m)O(n2m)

Code

#include <bits/stdc++.h>using namespace std;
using ll = long long;
const int maxn = 30 + 7, maxm = 5000 + 7, INF = 0x3f3f3f3f;int n, m, s, t, k;
int ans[maxn];
int g[maxn];
ll f[maxn][maxm];
int id[maxn];
int sum[maxn];
int a[maxn][maxm], b[maxn][maxm];void print(int n, int m)
{if(n == 0) return ;print(a[n][m], b[n][m]);if(a[n][m] == n)for (int i = 1; i <= n; ++ i)ans[id[i]] ++ ;else for (int i = a[n][m] + 1; i <= n; ++ i)ans[id[i]] = 1;
}int main()
{scanf("%d%d", &n, &m);for (int i = 1; i <= n; ++ i) {scanf("%d", &g[i]);id[i] = i;}sort(id + 1, id + 1 + n, [&](int a, int b){return g[a] > g[b];});memset(f, 0x3f, sizeof f);f[0][0] = 0;for (int i = 1; i <= n; ++ i) {sum[i] = sum[i - 1] + g[id[i]];for (int j = i; j <= m; ++ j) {f[i][j] = f[i][j - i];a[i][j] = i, b[i][j] = j - i;for (int k = 0; k < i; ++ k) {if(f[i][j] > f[k][j - (i - k)] + 1ll * k * (sum[i] - sum[k])) {f[i][j] = f[k][j - (i - k)] + 1ll * k * (sum[i] - sum[k]);a[i][j] = k, b[i][j] = j - (i - k);}}}}cout << f[n][m] << endl;print(n, m);for (int i = 1; i <= n; ++ i)printf("%d%c", ans[i], i == n ? '\n' : ' ');return 0;
}

0x51.动态规划 - 线性DP(习题详解 × 10)相关推荐

  1. c语言线性表库函数大全,数据结构(C语言版)-线性表习题详解

    <数据结构(C语言版)-线性表习题详解>由会员分享,可在线阅读,更多相关<数据结构(C语言版)-线性表习题详解(23页珍藏版)>请在人人文库网上搜索. 1.数 据 结 构 ,线 ...

  2. 0x52. 动态规划 - 背包(习题详解 × 19)

    目录 0x52. 动态规划 - 背包 0x52.1 0/10/10/1 背包 Problem A. 数字组合 Problem B. 背包问题求具体方案 Problem C. jury Compromi ...

  3. 【算法之动态规划(一)】动态规划(DP)详解

    一.基本概念 动态规划(dynamic programming)是 运筹学 的一个分支,是求解决策过程(decision process)最优化的数学方法.20世纪50年代初 美国 数学家R.E.Be ...

  4. 状态压缩动态规划部分习题详解

    状态压缩动态规划部分习题详解 状压DP部分题目详解 状态压缩动态规划部分习题详解 简介 经典子集类问题 原子弹 最短路与状压DP结合 送礼物 P3959宝藏 旅游 经典网格类 铺地砖 一笔画 其他类型 ...

  5. c语言将AOE网络的数据写入TXT文档中,数据结构与算法学习辅导及习题详解.张乃孝版-C/C++文档类资源...

    数据结构与算法学习辅导及习题详解.张乃孝版.04年10月 经过几年的努力,我深深体会到,编写这种辅导书要比编写一本湝通教材困难得多. 但愿我的上述理想,在本书中能够得以体现. 本书的组织 本书继承了& ...

  6. 数据结构(C语言版) 第 八 章 排序 知识梳理 + 习题详解

    目录 一.归并排序 二.交换排序 1.快速排序 2.冒泡排序 三.插入排序 1.直接插入排序(基于顺序查找) 2.折半插入排序(基于折半查找) 3.希尔排序(基于逐趟缩小增量) 四.选择排序 0.直接 ...

  7. 超级棒的一个DP问题详解(入门)

    超级棒的一个DP问题详解(入门) 只要耐心看肯定可以理解的~动态规划问题故事描述~ 通过金矿模型介绍动态规划 附上原文地址: http://www.cnblogs.com/sdjl/articles/ ...

  8. 数据结构(C语言版) 第 六 章 图 知识梳理 + 习题详解

    目录 一. 图的基本定义和术语 一.图的基本概念 1.度 2.连通 (1)连通图 (2)强连通/强连通图 3.回路 4.完全图 二.图的三种存储结构 1.邻接矩阵表示法 2.邻接表(链式)表示法 3. ...

  9. Python面对对象编程——结合面试谈谈封装、继承、多态,相关习题详解

    1.面向对象的三大特征 封装:属性和方法放到类内部,通过对象访问属性或者方法,隐藏功能的实现细节.当然还可以设置访问权限; 继承:子类需要复用父类里面的属性或者方法,当然子类还可以提供自己的属性和方法 ...

最新文章

  1. 微信小程序城市定位(百度地图API)
  2. 三星 Nexus S刷MIUI ROM最新图文刷机教程
  3. .describe() python_Python编程从入门到实践日记Day26
  4. vmware 打开虚拟机时提示“该虚拟机似乎正在被使用”解决
  5. Python中if条件判断语句的用法!
  6. 06-机器学习(Haar+Adaboost实现人脸、人眼检测)
  7. php接收二进制流,php接收二进制流【转】
  8. linux ls 时间显示时间格式,ls -l显示的日期格式如何设定?
  9. python复杂网络全局效率计算_python复杂网络库networkx:算法
  10. 3.0 面向对象 委托和事件 异常和错误
  11. c++ file* 句柄泄漏_C/C++连接MySql数据库使用总结
  12. [简单分页]C#+JQUERY+ORACLE分页效果 ----转载
  13. 在网络蚂蚁中设置代理服务器
  14. Internet连接共享只能上qq不能打开网页的问题解决
  15. 蓝桥杯 土地的面积计算
  16. Matlab中exp函数的使用
  17. 大四实习生java的个人计划
  18. jeesite下载excel模板
  19. 12V-5V-3.3V电源转换芯片
  20. 基于Android Studio的蓝牙通信的简单应用与开发

热门文章

  1. 【Python基础】手把手教你数据可视化!(附实例讲解)
  2. 技巧 | OpenCV中如何绘制与填充多边形
  3. 一文了解点特征直方图
  4. OpenCV图像旋转的原理与技巧
  5. 网络安全与机器学习(二):网络安全任务如何结合机器学习?
  6. [Math]理解卡尔曼滤波器 (Understanding Kalman Filter)
  7. windows 2012 exchange server 2013 搭建
  8. javascript_core_01之数据类型与运算
  9. 深入理解JavaScript系列(23):JavaScript与DOM(上)——也适用于新手
  10. 扫盲:关于Android手机内存ROM、RAM还有SD卡的解释