我也有在更新哦!
Upd 2020.9.20 经典例题 + 徒步旅行(travel)【第三周】
Upd 2020.9.21 经典例题 + some details
Upd 2021.1.11 数位dp + 状压dp + 浅谈期望dp
首先,什么是动态规划?
0x01 基本定义
动态规划(Dynamic Programming,DP) 是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼 (R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。
0x02 算法本身
是利用一些简单,好算的子问题去更新难一点的大问题,最后得到整体最优解。
其中,动态规划有三个步骤:
step 1 :定义状态,及我们常用的 (dp) 数组表示什么意义。
step 2 :初始化,就是把题目给出的,或者显而易见的子问题答案初始化。
step 3 :状态转移,我们需要找到一个状态转移方程去转移状态。或者也可以理解为如何利用已知子问题与下一个阶段的问题之间的联系去更新下一个阶段的问题的答案。
有点懵?那我们来看看一道例题
0x02.1 斐波那契数列
斐波那契数列 (1,1,2,3,5,8,13,21,34,55,...),从第三项起,每一项都是紧挨着的前两项的和。现在,请求出斐波那契的任意一项。
0x02.1-1 分析
我们按照动态规划的三步一步一步来。
首先,我们定义一个 (dp[i]) 表示斐波那契数列的第 (i) 项(一般定义状态就是题目要求什么我们就定义什么。
然后来思考如何初始化。你会发现题目已经给出了斐波那契数列的前几项,所以我们不妨按以下方式初始化。
dp[1] = 1; // 表示斐波那契数列的第一项为1
dp[2] = 1;
这时候再看,题目告诉我们“从第三项起,每一项都是紧挨着的前两项的和”。所以显然,这道题中的 (dp) 状态转移方程就是
dp[i] = dp[i - 1] + dp[i - 2]; // i >= 3
确定了这些,我们就可以写代码了嘛
不过 (dp) 的实现不止一种,我在这里介绍这道题的三种写法
0x02.1-2 AC代码1
常见的形式,本质是“别人更新自己”
#include <cstdio>
const int MAXN = 10005;
int dp[MAXN];
int main() {
int n;
scanf ("%d", &n);
dp[1] = 1;
dp[2] = 1;
for(int i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
printf("%d
", dp[n]);
return 0;
}
0x02.1-3 AC代码2
一般不用,本质是“自己更新别人”
#include <cstdio>
const int MAXN = 10005;
int dp[MAXN];
int main() {
int n;
scanf ("%d", &n);
dp[1] = 1;
dp[2] = 1;
// 因为我们的转移方程是 dp[i] = dp[i - 1] + dp[i - 2]
// 所以也可以看成 dp[i + 2] = dp[i + 1] + dp[i]
// 所以每次遇到 dp[i] 就让它的后两个更新即可
// 注意在 i = 1 的时候特判一下,不然就会重复更新 dp[2]
for(int i = 1; i <= n; i++) {
if(i != 1) dp[i + 1] += dp[i];
dp[i + 2] += dp[i];
}
printf("%d
", dp[n]);
return 0;
}
0x02.1-4 AC代码3
人气王,记忆化搜索,虽然好用,但是常数大
#include <cstdio>
const int MAXN = 10005;
int dp[MAXN];
int dfs(int i) { // 搜索
if(i <= 2) return 1;
if(dp[i]) return dp[i];
// 如果 dp[i] 现在已经有值了就可以直接返回,减少递归次数
return dp[i] = dfs(i - 1) + dfs(i - 2); // 否则更新
}
int main() {
int n;
scanf ("%d", &n);
printf("%d", dfs(n));
return 0;
}
经过一道简单题,有没有那么一点点懂什么是 (dp) 了?
接下来我们来看一些 (dp) 经典模型。
0x03 LIS
0x03.1 命题描述
给定一个整数序列 (A1A2A3….An)。求它的一个递增子序列(序列不要求元素连续),使子序列的元素个数尽量多。并输出其中一组递增子序列。
0x03.2 分析
这很简单嘛,按照 (dp) 三部曲。
我们先定义出 (dp[i]) 的含义,不妨假设 (dp[i]) 表示以 (i) 结尾的最长递增子序列的长度。那么一定有 (dp[1] = 1),这很显然吧。
接下来的问题就是如何转移了,根据我们 (dp) 数组的定义,我们会发现 (dp[i]) 应该是前面的某一个最长递增子序列加上 (a[i]) 得到的。而如果可以加上 (a[i]) 就代表上一个一定小于 (a[i])。那所以我们就可以直接在 (1) 到 (i - 1) 中找 (a[j] < a[i]) 并更新 (dp[i])。
那么,如何输出这组递增子序列呢?我们可以使用输出路径的技巧,利用 (pre) 数组保存前驱,然后递归输出即可。
0x03.3 具体实现
#include <cstdio>
const int MAXN = 5005;
int a[MAXN], dp[MAXN], prev[MAXN];
// dp[i]表示以i元素结尾的最长上升子序列
// prev[i]表示i号元素在最长上升子序列中的上一个元素的下标
void print(int i) { // 递归输出
if(prev[i] == i) {
printf("%d ", a[i]);
return ;
}
print(prev[i]);
printf("%d ", a[i]);
}
int main() {
int n;
scanf ("%d", &n);
int ans = 0, v = 0;
for(int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
dp[i] = 1; // 以i结尾的最长子序列最短就是只有i一个元素,所以初始化长度为1
prev[i] = i; // 前驱初始化为自己
for(int j = 1; j < i; j++) {
if(a[i] > a[j] && dp[j] + 1 > dp[i]) { // 在前面比当前元素小的元素处更新 dp[i]
dp[i] = dp[j] + 1; // 长度加一
prev[i] = j; // 保存前驱,即当前元素在这组最长上升子序列中的前面那一个元素
}
}
if(dp[i] > ans) {
ans = dp[i];
// 看以哪一个点结尾的最长子序列的长度最长
v = i; // 保存这个点的下标
}
}
printf("%d
", ans);
print(v);
return 0;
}
0x04 LCS
我谔谔,这就玄学不会打了?于是笔者回去翻了翻以前的代码……
0x04.1 命题描述
给定两个字符串,求出这两个字符串共同拥有的最长子序列。
0x04.2 分析
依然严格按照 (dp) 的三部曲慢慢来
首先我们定义 (dp[i][j]) 表示在 (a) 字符串中在 (i) 号元素前且在 (b) 字符串中在 (j) 号元素前的最长公共子序列。显然边界为 (dp[0][0] = 0)。
那么如何转移呢?显然,根据定义,如果当前枚举到的 (a[i]) 与 (b[j]) 相等,那么这两个元素一定可以和之前的最长公共子序列构成一个新的最长公共子序列。如果 (a[i]) 和 (b[j]) 不相等,那么就用这两个元素之前的最长公共子序列进行更新即可。(如果觉得玄学就一会儿看代码吧)
那么如何输出序列呢?也很简单,我们用一个 (flag) 记录当前更新是怎么更新的,然后在递归输出里特判一下就可以啦。
0x04.3 具体实现
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
char a[MAXN], b[MAXN];
int dp[MAXN][MAXN], flag[MAXN][MAXN];
// dp[i][j]表示在a[i]之前,b[j]之前的最长公共子序列,注意不是以a[i],b[j]结尾的最长公共子序列
// flag保存更新方式
void print(int i, int j) {
if(i == 0 || j == 0) return ; // 边界
if(flag[i][j] == 0) { // 如果是相等转移而来?递归两个字符串
print(i - 1, j - 1);
printf("%c", a[i - 1]); // 输出
}
else if(flag[i][j] == 1) print(i - 1, j); // 否则递归访问单个
else print(i, j - 1);
}
int main() {
scanf ("%s", a);
scanf ("%s", b);
int lena = strlen(a);
int lenb = strlen(b);
for(int i = 1; i <= lena; i++)
for(int j = 1; j <= lenb; j++) { // 遍历两个字符串
if(a[i - 1] == b[j - 1]) { // 如果枚举到当前元素相等
dp[i][j] = dp[i - 1][j - 1] + 1; // 那么直接就是之前的最大公共子序列的长度+1
flag[i][j] = 0; // 保存更新方式
}
else if(dp[i - 1][j] >= dp[i][j - 1]) {
// 如果当前元素不相等,且a数组目前的更长,那么就用a这边的更新
dp[i][j] = dp[i - 1][j];
flag[i][j] = 1;
}
else {
// 反之亦然
dp[i][j] = dp[i][j - 1];
flag[i][j] = -1;
}
}
printf("%d
", dp[lena][lenb]);
print(lena, lenb);
return 0;
}
0x05 背包
在考虑了一下后……最终选择把套娃背包提前了
0x05.1 0/1背包
有一个最多能装 (m) 千克的背包,有 (n) 件物品,它们的重量分别是 (W_1,W_2,...,W_n),它们的价值分别是 (C_1,C_2,...,C_n)。若每种物品只有一件,问能装入的最大总价值。
分析
显然,贪心很容易举出反例,毕竟你无法满足重量价值同时最优(啥,你说性价比最优?我谔谔
所以我们祭出动态规划。
定义一个 (dp[i][j]) 表示前 (i) 件物品放入容量为 (j) 的背包中能获得的最大价值。好了为什么这种背包叫0/1背包呢?因为它对于每件物品其实就是选与不选两种状态。因而我们可以很快得到转移方程:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + c[i]);
// 前者表示当前物品不取,其最大价值就是同样容量的背包下装入前i-1个物品的最大价值
// 后者表示当前物品要取。
// 其最大价值就是当前物品的价值加上去掉当前物品的重量的背包装前i-1件物品的最大价值
然后你会惊讶的发现,出现的 (dp) 数组的第一维均为 (i - 1),也就是说枚举到这件物品的时候,(dp) 一定是从上一件那里转移过来的,所以可以直接用当前物品覆盖上一件物品算出的答案,即删去第一维。没搞懂的可以用草稿纸画图推一下。。
具体实现
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
int dp[MAXN]; // 删去一维后的dp数组
struct node { // 定义一个结构体表示物品的信息
int w, c;
} a[MAXN];
int main() {
int n, v;
scanf ("%d %d", &n, &v);
for(int i = 1; i <= n; i++) scanf ("%d %d", &a[i].w, &a[i].c);
for(int i = 1; i <= n; i++) { // 枚举每件物品
for(int j = v; j >= a[i].w; j--)
// 枚举背包容量,并让背包容量严格大于当前物品的重量
// 按背包容量从大到小进行更新,不然更新顺序会错(不过二维好像就不存在这个问题了
// 可以自己推一下
dp[j] = max(dp[j], dp[j - a[i].w] + a[i].c);
}
printf("%d", dp[v]);
return 0;
}
0x05.2 完全背包
有一个最多能装 (m) 千克的背包,有 (n) 种物品,它们的重量分别是 (W_1,W_2,...,W_n),它们的价值分别是 (C_1,C_2,...,C_n)。若每种物品有无限件,问能装入的最大总价值。
分析
如果把它看成 0/1 的话。
定义 (dp[i][j]) 表示表示前 (i) 种物品放入容量为 (j) 的背包中能获得的最大价值。那么,显然可以通过 0/1 来推出这样一个式子
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - k * w[i]] + k * c[i]);
// k表示第i种物品放k个进背包
不过这就变成了一个及其低效的算法了。于是我们考虑优化。
显然,一种物品其实最多取 (v / w[i]) 件,所以我们可以直接将每种物品拆成 (v / w[i]) 件,转化为 0/1背包求解,但这不是最快的方法。
最快最玄学的完全背包的代码是这样的:
for(int i = 1; i <= n; i++) { // 枚举每件物品
for(int j = a[i].w; j <= v; j++)
dp[j] = max(dp[j], dp[j - a[i].w] + a[i].c);
}
哈?好像就是改了一下第二层循环的顺序?嗯。事实证明这是对的。首先,我们发现0/1背包如此使用循环顺序的原因是要满足即将更新的背包大小是没有加入任何一件第 (i) 件物品。而完全背包中,每种物品可以选很多件,就相当于我们需要在每次更新的时候即将更新的背包大小可能已经装进了几件第 (i) 种物品!所以把循环顺序反过来就好了。
第一维可以删掉的原因同上。
具体实现
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
int dp[MAXN];
struct data {
int c, w;
} a[MAXN];
int main() {
int m, n;
scanf ("%d %d", &m, &n);
for(int i = 1; i <= n; i++)
scanf ("%d %d", &a[i].w, &a[i].c);
for(int i = 1; i <= n; i++) {
for(int j = a[i].w; j <= m; j++)
dp[j] = max(dp[j], dp[j - a[i].w] + a[i].c);
}
printf("%d
", dp[m]);
return 0;
}
0x05.3 多重背包
有一个最多能装 (m) 千克的背包,有 (n) 种物品,它们的重量分别是 (W_1,W_2,...,W_n),它们的价值分别是 (C_1,C_2,...,C_n) 。若第 (i) 种物品有 (T_i) 件,问能装入的最大总价值。
分析
这种背包的解决方式其实在完全背包中已经提到了。我们把每种物品的每一件拆开来看,利用 0/1 背包求解。显然每一件都拆不是最优的方法。
于是,我们引入二进制拆分。首先,每个数都能被拆成 (2) 的乘法形式的和对吧(有能力有关系的同学可以尝试证明一下可以尝试去问问学数竞的同学)
那么,这和这道题有什么关系呢?很简单,我们可以把每种物品拆成这样的几件:重量为 (W_i), 价值为 (C_i);重量为 (W_i imes 2), 价值为 (C_i imes 2);重量为 (W_i imes 4), 价值为 (C_i imes 4)……以此类推,相当于就是把每一种的件数二进制拆分而组合起来。
然后0/1即可。
具体实现
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
struct node {
int w, v; // 这里的v就是上文的c,即价值
} a[MAXN];
int dp[MAXN];
int main() {
int m, n, cnt = 0;
scanf ("%d %d", &m, &n);
for(int i = 1; i <= n; i++) {
int x, y, z;
scanf ("%d %d %d", &x, &y, &z);
for(int j = 1; j <= z; j <<= 1) { // 二进制拆分
a[++cnt].w = x * j;
a[cnt].v = y * j;
z -= j;
}
if(z) { // 最后遗留下来的全部合起来作为下一组
a[++cnt].w = x * z;
a[cnt].v = y * z;
}
}
for(int i = 1; i <= cnt; i++)
for(int j = m; j >= a[i].w; j--)
dp[j] = max(dp[j], dp[j - a[i].w] + a[i].v); // 01背包
printf("%d", dp[m]);
return 0;
}
好了基础的背包就到这里了,剩下的其实就是这三种背包的反复运用。可以自己去分析一下(在这里就直接口胡一下命题……
比较出名的有:
0x05.4 混合背包
顾名思义,将几种背包混合起来。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
int w[MAXN], v[MAXN], m[MAXN];
int dp[MAXN * 100];
int main() {
int v_, n;
scanf("%d %d", &n, &v_);
for(int i = 1; i <= n; i++)
scanf("%d %d %d", &w[i], &v[i], &m[i]);
for(int i = 1; i <= n; i++) {
if(m[i] == 0) {
for(int j = w[i]; j <= v_; j++)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
else {
int x = m[i];
if(m[i] == -1) x = 1;
for(int k = 1; k <= x; k <<= 1) {
for(int j = v_; j >= w[i] * k; j--)
dp[j] = max(dp[j], dp[j - w[i] * k] + v[i] * k);
x -= k;
}
if(x) {
for(int j = v_; j >= w[i] * x; j--)
dp[j] = max(dp[j], dp[j - w[i] * x] + v[i] * x);
}
}
}
printf("%d", dp[v_]);
return 0;
}
0x05.5 分组背包
每个物品有它的组,且每一组只能选一件物品。
#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN = 105;
int dp[MAXN], v[MAXN], w[MAXN];
int n, m;
int main() {
scanf ("%d %d", &n, &m);
for(int i = 1; i <= n; i++) {
int t;
scanf ("%d", &t);
for(int j = 1; j <= t; j++)
scanf ("%d %d", &v[j], &w[j]);
for(int j = m; j >= 0; j--)
for(int k = 1; k <= t; k++)
if(j >= v[k])
dp[j] = max(dp[j], dp[j - v[k]] + w[k]);
}
printf("%d", dp[m]);
return 0;
}
0x05.6 二位费用背包
一件物品拥有两种价值,双重循环其实就可以了
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int MAXN = 105;
const int MAXM = 1205;
int dp[MAXM][MAXM];
struct node {
int a, b, c;
} s[MAXN];
int main() {
int u, v, n;
scanf ("%d %d %d", &u, &v, &n);
for(int i = 1; i <= n; i++)
scanf ("%d %d %d", &s[i].a, &s[i].b, &s[i].c);
memset(dp, 0x3F, sizeof dp);
dp[0][0] = 0;
for(int i = 1; i <= n; i++)
for(int j = u + 100; j >= s[i].a; j--)
for(int k = v + 100; k >= s[i].b; k--)
dp[j][k] = min(dp[j][k], dp[j - s[i].a][k - s[i].b] + s[i].c);
for(int i = u; i <= u + 100; i++)
for(int j = v; j <= v + 100; j++)
if(dp[i][j] != 0x3F3F3F3F) {
printf("%d
", dp[i][j]);
return 0;
}
printf("-1");
return 0;
}
0x06 区间dp
顾名思义,每次转移一个区间的答案到另一个区间。
玄学?那我们来看一道经典题目
0x06.1 石子合并
0x06.1-1 题目描述
(n) 堆石子摆成一条线。现要将石子有次序地合并成一堆。规定每次只能选相邻的 (2) 堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。计算将 (n) 堆石子合并成一堆的最小代价。
0x06.1-2 样例输入
4
1 2 3 4
0x06.1-3 样例输出
19
0x06.1-4 分析
首先我们来定义状态,(dp[i][j]) 表示将 (i) 到 (j) 堆石子合并成一堆最少需要多少的代价。且 (dp[i][i] = 0)。
然后我们会发现,如果要将所有的石子合成一堆,那一定最后会先合成两堆,再合成一堆。仔细一想将两堆合成一堆的代价就是,分别合成这两堆的代价加上这两堆石子数总和。以此就得到了状态转移方程。
不过这一题的更新的循环有点不同,区间 (dp) 是先枚举要更新的区间的长度(即阶段,当然阶段最开始长度为1),再依靠阶段和我们枚举的第二层循环 (i) 去找到 (j) 的位置
0x06.1-5 具体实现
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
int a[MAXN], dp[MAXN][MAXN];
// dp[i][j]表示将第i堆石子到第j堆石子全部合并需要的最小代价
int sum[MAXN];
int main() {
int n;
memset(dp, 0x3F3F3F3F, sizeof(dp));
// 因为求最小嘛,所以需要初始化为最大值
scanf ("%d", &n);
for(int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
dp[i][i] = 0;
sum[i] = sum[i - 1] + a[i];
// 保存前缀和,因为我们的状态转移方程涉及到区间和
}
for(int len = 2; len <= n; len++) // 枚举阶段
for(int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1; // 相当于i是区间左端点,我们需要算出右端点j
for(int k = i; k < j; k++)
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
// 分成两部分求最小
}
printf("%d", dp[1][n]); // 输出答案即可
return 0;
}
0x07 排列dp
这是一个非常玄学的 (dp),它主要研究的是关于数列排列的问题。给个例子叭。
0x07.1 命题描述
给定一个数列 (1) 到 (n),请问有多少种排列使得排列后的数列所含逆序对为偶数。
0x07.2 分析
首先,这道题的答案应该是 (n! / 2)(我也不知道为什么)。不过我们在这里不讲这个方法的证明,而是引入排列 (dp)。
我们定义 (dp[i][j]) 为排好前 (i) 个数能产生 (j) 个逆序对的总方案数。显然,(dp[1][0] = 1)。
那么,该如何转移呢?其实我们可以把 (i + 1) 看作是插入前 (i) 个数排好的序列。那么我们考虑插入到某个位置会带来多少逆序对即可。
会发现 (i + 1) 一定大于前面的每一个数,所以当 (i + 1) 插入到第 (k) 个位置时,将会新产生 (i - k) 个逆序对。所以我们只需要遍历一遍 (i),(j)。在每次枚举一个 (k),转移即可。
0x07.3 具体实现
#include <cstdio>
const int MAXN = 1005;
int dp[MAXN][MAXN];
// dp[i][j]表示前i个数能存在j个逆序对的排列方案总数
int main () {
int n;
scanf ("%d", &n);
dp[1][0] = 1; // 初始化
for(int i = 1; i < n; i++) /
for(int j = 0; j <= n * (n - 1) / 2; j++)
for(int k = 0; k <= i; k++) // 分别枚举i,j,k
dp[i + 1][j + i - k] += dp[i][j];
// 把i+1插入到k会产生i-k个新的逆序对
// 更新新的总方案数答案
int ans = 0;
for(int i = 0; i <= n * (n - 1) / 2; i += 2) // 统计能得到偶数个逆序对的总方案数
ans += dp[n][i]; // 累加所有数的合法排列方案总数
printf("%d", ans);
return 0;
}
不过……这时间复杂度也太高了?
于是我们考虑优化。会发现题目要求的是求可以得到偶数个逆序对的方案数,而不需要求出具体每个方案可以得到多少逆序对。所以我们可以优化第二维,改为取模。具体看代码叭。。
0x07.4 优化
#include <cstdio>
const int MAXN = 1005;
int dp[MAXN][2];
// 这时的dp[i][j]表示前i个数能得到偶数或奇数个逆序对的方案总数
// j=1表示奇数个逆序对,j=0表示偶数个
int main () {
int n;
scanf ("%d", &n);
dp[1][0] = 1;
for(int i = 1; i < n; i++)
for(int j = 0; j <= 1; j++)
for(int k = 0; k <= i; k++)
dp[i + 1][(j + i - k) % 2] += dp[i][j];
printf("%d", dp[n][0]); // 这里就不用累加了,直接输出即可
return 0;
}
嘛,这就很快了。
我也许会鸽,但一定不会弃稿(逃
一维不行就二维,二维不行就三维……总有一年你能把状态转移方程想出来的(长者。
0x08 树形DP
0x08.1-1 经典题目描述
Ural大学有N名职员,编号为1~N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
0x08.1-2 输入格式
第一行一个整数N。
接下来N行,第 i 行表示 i 号职员的快乐指数Hi。
接下来N-1行,每行输入一对整数L, K,表示K是L的直接上司。
0x08.1-3 输出格式
输出最大的快乐指数。
样例输入
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0x08.1-4 样例输出
5
0x08.1-5 分析
其实就是每次遍历子树,并更新状态即可。(字面意思
0x08.1-6 AC代码
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 6005;
int dp[MAXN][2], r[MAXN];
vector<int> s[MAXN];
bool vis[MAXN];
void Tree_Dp(int i) {
dp[i][1] = r[i];
dp[i][0] = 0;
for(int j = 0; j < s[i].size(); j++) {
Tree_Dp(s[i][j]);
dp[i][1] += dp[s[i][j]][0];
dp[i][0] += (max(dp[s[i][j]][0], dp[s[i][j]][1]));
}
return ;
}
int main() {
int n, ans = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf ("%d", &r[i]);
for(int i = 1; i <= n - 1; i++) {
int u, v;
scanf ("%d %d", &u, &v);
s[v].push_back(u);
vis[u] = true;
}
int root;
for(int i = 1; i <= n; i++)
if(vis[i] == false) {
root = i;
break;
}
Tree_Dp(root);
printf("%d", max(dp[root][1], dp[root][0]));
return 0;
}
0x08.2 求树的直径
基本思想:(ans_i) 表示过 (i) 节点的最长链。
(dp_i) 表示以 (i) 节点为端点的最长链。
若 (u) 为 (v) 的父亲节点,则显然有 (ans_u = mathrm{max}{dp_u + dp_v}, dp_u = mathrm{max}{dp_v + 1})
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
const int INF = 0x3f3f3f3f;
int n;
int dp[MAXN], ans[MAXN], len, head[MAXN];
struct edge {
int to, next;
} edge[MAXN * 2];
void init() {
memset(ans, -INF, sizeof ans);
}
void Add_Edge(int x, int y) {
edge[++len].to = y;
edge[len].next = head[x];
head[x] = len;
}
void Tree_Dp(int u, int fa) {
for(int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if (v == fa)
continue;
Tree_Dp(v, u);
ans[u] = max(ans[u], dp[u] + dp[v]);
dp[u] = max(dp[u], dp[v] + 1);
}
}
int main() {
scanf("%d", &n);
init();
for (int i = 1; i < n; i++) {
int x, y;
scanf("%d %d", &x, &y);
Add_Edge(x, y);
Add_Edge(y, x);
}
Tree_Dp(1, -1);
int cnt = -INF;
for(int i = 1; i <= n; i++)
cnt = max(cnt, ans[i]);
printf("%d", cnt + 1);
return 0;
}
0x08.3 求树的重心
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 105;
vector<int> map[MAXN];
int dp[MAXN], f[MAXN], flag[MAXN];
int n;
bool vis[MAXN];
int len = 0, ans[3];
void Tree_Dp(int i) {
vis[i] = true;
dp[i] = 1;
for (int j = 0; j < map[i].size(); j++) {
int v = map[i][j];
if (vis[v])
continue;
Tree_Dp(v);
dp[i] += dp[v];
if (dp[v] > n / 2)
flag[i] = true;
else
f[i] = max(f[i], dp[v]);
}
if (n - dp[i] > n / 2)
flag[i] = true;
f[i] = max(f[i], n - dp[i]);
return;
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
map[u].push_back(v);
map[v].push_back(u);
}
Tree_Dp(1);
int mi = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
if (f[i] <= mi && !flag[i])
mi = f[i];
// printf("%d
", f[i]);
}
for (int i = 1; i <= n; i++)
if (f[i] == mi)
ans[++len] = i;
printf("%d
", len);
for (int i = 1; i <= len; i++) printf("%d
", ans[i]);
return 0;
}
0x09 状压dp
0x09.1 基本定义
简单来说,就是我们在 dp 表示的状态上下功夫。
对于一个状态,如果很难用常规 dp 的那种“有多少变量开几维”的方式表示,则我们就需要用状压。
状压,即状态压缩,它将一些状态转化为某一些进制的数,方便我们去转移。
很玄乎?我们上到例题/xyx
0x09.2 例题
这道题算藏得很深的状压 dp 了。
首先,对于每一行,我们需要考虑这样的状态:每一个位置是否放炮兵。
那么我们是否可以用二进制数来表示这个状态呢?
如果一个数,在二进制下,所有当前状态放了炮兵的位置为 (1),其余为 (0),显然我们就达到了表示状态的目的。
现在我们来考虑什么样的状态合法。
显然,若状态 (x) 的二进制位中有两个 (1) 之间的距离小于 (2),则这个状态一定不合法,这是题目要求的。
现在来定义 (dp_{i, j}) 表示第 (i) 行,状态是 (j) 时,最多能放的炮兵数。
首先我们一定需要保证 (j) 属于合法状态。
其次,考虑哪些之前的状态会对 (dp_{i, j}) 产生贡献。显然如果 (i - 1) 行状态为 (k) 且 (k & j eq 0),则对于 (k) 来说,(j) 不是个合法状态,这就是原题目跨行的限制。(即相邻三行不能在同一列上放炮兵。
但原题目的限制是跨的两行,所以我们需要调整一下 (dp)。
设 (dp_{i, j, k}) 表示第 (i) 行状态为 (j) ,上一行(没有就不管)的状态为 (k) 的最大放炮兵数。
显然 (dp_{i, j, k} = mathrm{max}{dp_{i - 1, k, l}, l & k = 0, k & j = 0} + w_j)
其中 (w_j) 表示 (j) 在二进制中有多少个 (1),也就是当前这一行的状态对答案的贡献。
当然在状态转移方程中,我们还需保证 (l, k, j) 均为合法状态。
状态显然是在 (0) 到 (2^n) 间枚举。
细节较多,慢慢处理即可。因为会出现二进制,所以使用了大量位运算。
在以二进制为状态的题目中,理论上位运算越简洁你的状压越快。
0x09.3 具体实现
#include <cstdio>
int read() {
int k = 1, x = 0;
char s = getchar();
while (s < '0' || s > '9') {
if (s == '-')
k = -1;
s = getchar();
}
while (s >= '0' && s <= '9') {
x = (x << 3) + (x << 1) + s - '0';
s = getchar();
}
return x * k;
}
void write(int x) {
if(x < 0) {
putchar('-');
x = -x;
}
if(x > 9)
write(x / 10);
putchar(x % 10 + '0');
}
int Max(int x, int y) {return x > y ? x : y;}
const int MAXN = 101;
const int MAXM = 11;
const int MAXL = (1 << 10);
char s[MAXN];
bool mp[MAXN][MAXM], vis[MAXN][MAXL];
int dp[2][MAXL][MAXL], w[MAXL];
int main() {
int n = read(), m = read();
for(int i = 1; i <= n; i++) {
scanf ("%s", s + 1);
for(int j = 1; j <= m; j++)
mp[i][j] = ((s[j] == 'P') ? 1 : 0);
}
for(int i = 0; i < (1 << m); i++)
for(int j = 0; j < m; j++)
if((i >> j) & 1)
w[i]++;
for(int i = 1; i <= n; i++)
for(int j = 0; j < (1 << m); j++) {
bool flag = true;
int last = -1;
for(int k = 0; k < m; k++)
if((j >> k) & 1) {
if((last != -1 && k - last + 1 < 3) || !mp[i][k + 1]) {
flag = false;
break;
}
last = k;
}
vis[i][j] = flag ? 1 : 0;
}
for(int i = 0; i <= 1; i++)
for(int j = 0; j < (1 << m); j++)
for(int k = 0; k < (1 << m); k++)
dp[i][j][k] = -0x3f3f3f3f;
dp[0][0][0] = 0;
for(int i = 1; i <= n; i++)
for(int j = 0; j < (1 << m); j++) {
if(!vis[i][j] || j & (j << 1) || j & (j << 2))
continue;
for(int k = 0; k < (1 << m); k++) {
if(j & k || (!vis[i - 1][k] && j <= 1) || k & (k << 1) || k & (k << 2))
continue;
for(int l = 0; l < (1 << m); l++)
if((i > 2 ? vis[i - 2][l] : 1)
&& !(j & l) && !(l & k))
dp[i % 2][j][k] = Max(dp[i % 2][j][k], dp[(i - 1) % 2][k][l] + w[j]);
}
}
int ans = 0;
for(int i = 0; i < (1 << m); i++) {
if(!vis[n][i])
continue;
for(int j = 0; j < (1 << m); j++)
if(!(i & j) && (n > 1 ? vis[n - 1][j] : 1))
ans = Max(ans, dp[n % 2][i][j]);
}
write(ans);
return 0;
}
0x10 期望dp
0x10.1 前置芝士
关于期望。
我们定义期望 (E(X) = sum_i X_i P_i),其中 (X_i) 表示第 (i) 个事件发生的价值,(P_i) 表示第 (i) 个事件发生的概率。
同时我们记概率为 (Pr(X)),显然这是一个从事件到实数的映射。
通常,(Pr(x) leq 0),特别的,若 (E = {X}),(E) 为全集,则 (Pr(E) = 1)。也就是所有事件发生概率之和为 (1)。证明显然。
part1. 期望具有可加性。(E(X + Y) = E(X) + E(Y))。
证明:(LHS = sum_{i, j} (X_i + Y_i) P_i Q_j)
(LHS = sum_{i, j} X_i P_i Q_j + sum_{i, j} Y_j P_i Q_j)
(LHS = sum_i X_i P_i sum_j Q_j + sum_j Y_j Q_j sum_i P_i)
由概率的性质, (sum_j Q_j = sum_i P_i = 1)
所以 (E(X + Y) = sum_i X_i P_i + sum_j Y_j Q_j = E(X) + E(Y))
part2. 期望具有可乘性。(E(XY) = E(X) E(Y))。
证明:(LHS = sum_{i, j} X_i Y_j P_i Q_j)
(LHS = sum_i X_i P_i sum_j Y_j Q_j)
即 (E(XY) = sum X_i P_i sum_j Y_j Q_j = E(X) E(Y))
part3. 期望具有常数可乘性。(E(aX) = aE(X))
证明:(LHS = sum_i a X_i P_i)
(LHS = a sum_i X_i P_i)
即 (E(aX) = a sum_i X_i P_i = a E(X))
part4. 可以利用数归推广 part2 和 part3。
(E(sum_i A_i) = sum_i A_i S_i, A = {X, Y, Z dots })
(E(prod_i A_i) = prod_i A_i S_i, A = {X, Y, Z dots })
0x10.2 例题
借这道题来讲讲实践。
我们通常会利用dp解决一些期望问题。即我们所说的期望dp。
但这里的dp本质上转移与期望没有关系,只是它做的工作与期望有关。
设 (dp_i) 表示 当前已选了 (i) 种点数,还需一直选到 (n) 种点数的丢骰子数的期望。
那么初始 (dp_n = 0),答案为 (dp_0)。
则每次丢骰子有两种状态。
- 和之前的点数一样,有 (frac i n) 的概率出现。记为 (X)。
- 和之前的点数不一样,有 (frac {n - i} n) 的概率出现。记为 (Y)。
那么所以当前这一状态 (E(i) = E(X) + E(Y))
那么根据定义,(X) 的概率为 (frac i n),价值为 (dp_i),(Y) 的 概率为 (frac {n - i} n),价值为 (dp_{i + 1})。
所以 (dp_i = frac {n - i} n dp_{i + 1} + frac i n dp_i)。
而因为我们的 (dp_i) 表示的与丢骰子数有关,而每种状态一定都会丢一次,所以我们的 (dp_i) 还需累加 (1)。
于是我们得到式子: (dp_i = frac {n - i} n dp_{i + 1} + frac i n dp_i + 1)
然后去解上式。
上式可化为:(frac {n - i} n dp_i = frac {n - i} n dp_{i + 1} + 1)。
因为 (frac {n - i} n eq 0)。
所以得到 (dp_i = dp_{i + 1} + frac n {n - i})。
然后我们来简化这个式子。
(dp_i = dp_{i + 2} + frac n {n - i - 1} + frac n {n - i})
(dp_i = dp_n + sum_{j = 1}^{n - i}frac n {n - i - j + 1})
所以 (dp_0 = sum_{j = 1}^n frac n {n - j + 1})
即 (dp_0 = sum_{j = 1}^n frac n j),就不给实现了吧。。。
0x11 经典题目
T1 猪猪储蓄罐
其实很水,但放进来是因为它非常的经典。。。
方向的求最小值只需要把 (dp) 数组初始化为最大值即可(然后就完全背包板了
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 10005;
int p[MAXN], w[MAXN];
int dp[MAXN];
int main() {
memset(dp, 0x3f, sizeof dp);
// 初始最大值
int E, F;
scanf ("%d %d", &E, &F);
F -= E; // 直接减去储蓄罐本身的重量
int n;
scanf ("%d", &n);
for(int i = 1; i <= n; i++)
scanf ("%d %d", &p[i], &w[i]);
dp[0] = 0;
// 完全背包
for(int i = 1; i <= n; i++)
for(int j = w[i]; j <= F; j++)
dp[j] = min(dp[j - w[i]] + p[i], dp[j]); // 取最小值
if(dp[F] == 0x3f3f3f3f) {
printf("This is impossible.
");
return 0;
}
printf("The minimum amount of money in the piggy-bank is %d.
", dp[F]);
return 0;
}
T2 硬币问题
本人太弱了,时间复杂度被按着摩擦。
首先,这道题类似多重背包,所以我们转换为01背包来求解。
定义 (dp[i]) 表示 (i) 能否被组成,那么一定会有
dp[j] = (dp[j] | dp[j - a[i]]); // a[i]为枚举的当前硬币
别告诉我你不知道逻辑或运算
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXN = 100005;
int len = 0, a[MAXN], A[MAXN], c[MAXN];
bool dp[MAXN];
int main() {
int n, m;
while(scanf("%d %d", &n, &m) != EOF) {
if(n == 0 && m == 0)
return 0;
memset(dp, 0, sizeof dp); // 初始化。。。
len = 0;
for(int i = 1; i <= n; i++)
scanf ("%d", &A[i]);
for(int i = 1; i <= n; i++)
scanf ("%d", &c[i]); // 存储每种硬币的数量
for(int i = 1; i <= n; i++)
for(int j = 1; j <= c[i]; j++)
// 转换为01背包问题
a[++len] = A[i];
dp[0] = true; // 0一定能凑出来对吧
for(int i = 1; i <= len; i++)
for(int j = m; j >= a[i]; j--)
dp[j] = (dp[j] | dp[j - a[i]]);
int ans = 0;
for(int i = 1; i <= m; i++) // 遍历找出共有多少个能被组成
if(dp[i])
ans++;
printf("%d
", ans);
}
return 0;
}
T3 投资 & 收益
我愿称它为背包容量在变化的背包问题。
这道题只需要枚举每一天,每一天都做一次完全背包,找到最优方案,在把收益叠加进下一天的背包容量。
不过需要一个优化,因为题目说“债券的价值始终是 1000 美元的倍数”,所以我们在枚举背包大小时,可以直接除以1000
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
int dp[MAXN], w[MAXN], c[MAXN];
int main() {
int T;
scanf ("%d", &T);
while(T--) {
memset(dp, 0, sizeof dp); // 初始化
int v, y, n;
scanf ("%d %d", &v, &y);
scanf ("%d", &n);
for(int i = 1; i <= n; i++)
scanf ("%d %d", &w[i], &c[i]);
for(int k = 1; k <= y; k++) {
// 枚举这是第几天了
for(int i = 1; i <= n; i++)
for(int j = w[i] / 1000; j <= v / 1000; j++) // 完全背包板 + 容量优化
dp[j] = max(dp[j], dp[j - w[i] / 1000] + c[i]);
v += dp[v / 1000]; // 改变下一天的背包容量
}
printf("%d
", v);
}
return 0;
}
T4 三角形牧场
我们直接考虑,当前的这些木板可以组成哪些三角形。
这里用到二维费用背包,定义 (dp[i][j]) 表示一个三边长分别为 (i, j, sum - i - j)的三角形是否可行。((sum)表示所有木板的总长。
如果 (dp[i][j]) 可行, 那对于我们枚举的 (a[k]) 一定存在 (dp[i][j - a[k]]) 或者 (dp[i - a[k][j]]) 可行。
显然三角形三边之长不能大于 (sum / 2),二重循环状态转移即可。
for(int i = 1; i <= n; i++)
for(int j = sum / 2; j >= 0; j--)
for(int k = sum / 2; k >= 0; k--)
if((j >= a[i] && dp[j - a[i]][k]) || (k >= a[i] && dp[j][k - a[i]]) )
dp[j][k] = true;
得到所有可行三角形后,海伦公式暴算找到最大值即可。(注意精度
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 1605;
bool dp[MAXN][MAXN];
int a[MAXN];
bool check(int x, int y, int z) {
if (x + y > z && y + z > x && x + z > y)
return true;
return false;
}
double S(int x, int y, int z) {
double t = (x + y + z) / 2.0;
return (double)sqrt(t * (t - x) * (t - y) * (t - z));
}
int main() {
int n, sum = 0;
scanf ("%d", &n);
for(int i = 1; i <= n; i++) {
scanf ("%d", &a[i]);
sum += a[i];
}
dp[0][0] = true;
for(int i = 1; i <= n; i++)
for(int j = sum / 2; j >= 0; j--)
for(int k = sum / 2; k >= 0; k--)
if((j >= a[i] && dp[j - a[i]][k]) || (k >= a[i] && dp[j][k - a[i]]) )
dp[j][k] = true;
double ma = 0;
for(int i = 1; i <= sum / 2; i++)
for(int j = 1; j <= sum / 2; j++)
if(check(i, j, sum - i - j) && dp[i][j])
ma = max(ma, S(i, j, sum - i - j));
int ans = (int)(ma * 100);
if(ans == 0) {
printf("-1
");
return 0;
}
printf("%d
", ans);
return 0;
}
Q 徒步旅行
题目描述
小 A 决定开始一场奇妙的徒步旅行,旅行地图可以看成是一个平面直角坐标系,小 A 从家 (O(0, 0)) 出发,每一步移动只能由他此时所在的位置 ((x, y)) 走到以下四个坐标之一:((x - 1, y), (x, y - 1), (x + 1, y), (x, y + 1))。
现在有 (n) 个旅游景点,第 (i) 个旅游景点位置为 ((x_i, y_i)) 。
由于世界如此之大,整个旅行地图被分成了多个不同的气候区,某个景点 ((x_i, y_i)) 的气候区 (C_i = max(x_i, y_i))。小 A 想要更好的了解这个世界使得他这次徒步旅行更有意义,所以他想要去气候区 旅行当且仅当访问完气候区为 (i + 1) 的所有旅游景点。当他访问完所有的景点时,他会回到家里。
小 A 想让你帮他设计出一条旅游路线使得他移动的步数最少,因为徒步旅行还是比较累的……
输入格式
第一行输入一个整数 (n),表示旅游景点数量。
接下来 (n) 行,每行一个整数对 ((x_i, y_i)) 代表第 (i) 个景区的位置。
输出格式
仅一行,表示小 A 完成旅行所需移动的最少步数。
样例输入1
8
2 2
1 4
2 3
3 1
3 4
1 1
4 3
1 2
样例输出1
20
样例输入2
5
2 1
1 0
2 0
3 2
0 3
样例输出2
12
数据范围与提示
样例解释1
依次访问 0-6-8-1-4-3-7-5-2-0 号旅游景点即可。
其中 (0) 号景点代表家。
分析
首先,对于每个气候区显然它会是一个类似这样的图形:(当然,肯定不是每个1都是景点。
0 0 0 0 0
1 1 1 1 0
0 0 0 1 0
0 0 0 1 0
0 0 0 1 0
利用贪心的思想,走每一个气候区的时候,为了路程最短,一定不能走重复的点(对于当前气候区)
因为要把整个需要走的气候区走完才能走下一个气候区,所以对于每个气候区我们只能有两种方式走完。
假设现在在气候区3里有这些点
0 0 0 0 0
0 0 0 0 0
1 0 1 0 0
0 0 1 0 0
0 0 0 0 0
那么我们只有这些走法:(这时的标号表示顺序。
0 0 0 0 0
0 0 0 0 0
1 0 2 0 0
0 0 3 0 0
0 0 0 0 0
或
0 0 0 0 0
0 0 0 0 0
3 0 2 0 0
0 0 1 0 0
0 0 0 0 0
所以每一个气候区我们考虑是从左上到右下,还是右下到左上。于是乎就有了贪心第一次尝试。
每次考虑下一次走哪条红线到下一个气候区。
事实证明,60pt。
#include <cstdio>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 300005;
struct node {
int x, y, c, index;
node() {}
node(int X, int Y) {
x = X;
y = Y;
}
} s[MAXN], s1[MAXN];
int dis(node a, node b) { return abs(a.x - b.x) + abs(a.y - b.y); }
bool cmp(node a, node b) {
if (a.c == b.c) {
if (a.x == b.x)
return a.y > b.y;
else
return a.x < b.x;
} else
return a.c < b.c;
}
bool cmp1(node a, node b) {
if (a.c == b.c) {
if (a.y == b.y)
return a.x > b.x;
else
return a.y < b.y;
} else
return a.c < b.c;
}
int main() {
int n;
scanf("%d", &n);
s[0].x = 0, s[0].y = 0, s[0].c = 0;
s1[0].x = 0, s1[0].y = 0, s1[0].c = 0;
bool k = false;
for (int i = 1; i <= n; i++) {
scanf("%d %d", &s[i].x, &s[i].y);
if (s[i].x != s[i].y)
k = true;
s[i].index = i;
s[i].c = max(s[i].x, s[i].y);
s1[i] = s[i];
}
long long ans = 0;
if (!k) {
sort(s, s + n + 1, cmp);
for (int i = 1; i <= n; i++) ans += dis(s[i], s[i - 1]);
ans += dis(s[n], s[0]);
printf("%lld
", ans);
return 0;
}
sort(s, s + n + 1, cmp);
sort(s1, s1 + n + 1, cmp1);
node now;
now.x = 0, now.y = 0;
bool flag;
for (int i = 1; i <= n; i++) {
if (s[i].c != s[i - 1].c) {
if (dis(now, s[i]) < dis(now, s1[i]))
flag = true;
else
flag = false;
}
if (flag) {
ans += dis(s[i], now);
now.x = s[i].x;
now.y = s[i].y;
} else {
ans += dis(s1[i], now);
now.x = s1[i].x;
now.y = s1[i].y;
}
}
ans += now.x;
ans += now.y;
printf("%lld
", ans);
return 0;
}
显然,贪心只能考虑局部最优,但这道题需要整体最优。
于是考虑动态规划。
定义 (dp[i][1]) 表示第 (i) 个气候区是从左上走到右下。
定义 (dp[i][0]) 表示第 (i) 个气候区是从右下走到左上。
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <iostream>
using namespace std;
const int MAXN = 300005;
struct node {
int x, y, c;
} s[MAXN];
int dis(int i, int j) {
return abs(s[i].x - s[j].x) + abs(s[i].y - s[j].y);
}
bool cmp(node a, node b) {
if(a.c == b.c) {
if(a.x == b.x)
return a.y > b.y;
return a.x < b.x;
}
return a.c < b.c;
}
/*
0 0 0 0 0
0 0 1 0 0
0 2 0 0 0
0 0 4 0 0
0 0 0 3 0
transform to...
0 0 0 0 0
0 0 3 0 0
0 1 0 0 0
0 0 2 0 0
0 0 0 4 0
*/
int l[MAXN], r[MAXN];
// l, r 存气候中左右端点标号
// dist 存储走完整个气候最短长度
long long dp[MAXN][2], dist[MAXN];
int main() {
int n;
scanf ("%d", &n);
// cerr << n << endl;
s[0].x = 0;
s[0].y = 0;
s[0].c = 0;
for(int i = 1; i <= n; i++) {
scanf ("%d %d", &s[i].x, &s[i].y);
// cerr << s[i].x << " " << s[i].y << endl;
s[i].c = max(s[i].x, s[i].y);
}
int len = 0;
sort(s + 1, s + n + 1, cmp);
for(int i = 1; i <= n; i++) {
if(s[i].c != s[i - 1].c) { // 跨气候了
l[++len] = i;
r[len] = i;
}
else { // 气候内
r[len] = i;
dist[len] += dis(i, i - 1);
}
}
for(int i = 1; i <= len; i++) {
dp[i][0] = min(dp[i - 1][0] + dis(l[i - 1], r[i]), dp[i - 1][1] + dis(r[i - 1], r[i])) + dist[i];
dp[i][1] = min(dp[i - 1][0] + dis(l[i - 1], l[i]), dp[i - 1][1] + dis(r[i - 1], l[i])) + dist[i];
}
printf("%lld
", min(dp[len][0] + dis(0, l[len]), dp[len][1] + dis(0, r[len])));
return 0;
}