2025.3.19-3.30 学习随记DP(KMP、AC自动机穿插)
持续了十天左右的 DP 章终于告一段落,感觉很不对哇,照这个速度,我岂不是很快就把提高级知识点过完了?
总的来说,巩固得应当是不错的。
章节内容较多,可以分作多部分来总结。
1.数字三角形模型
比较简单,但是是线性 DP 部分。
感觉这个模型一般都是签到题,或者 \(T2\) 的样子。
注意的一个点就是在 \(2\) 条路线,或者说多条路线的时候,要关注到一个性质,就是你同一时间每条路线所在的点横纵坐标之和必定是相等的,也许等于你的 \(t\),但是可能有点权之类的。
[NOIP 2000 提高组] 方格取数
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = N << 1;
int n;
int a[N][N];
int f[N][N][N];
signed main()
{
cin >> n;
int x, y, v;
while (cin >> x >> y >> v, x || y || v) a[x][y] = v;
for (int i = 2; i <= n + n; i ++ )
for (int x1 = max(1, i - n); x1 <= min(n, i - 1); x1 ++ )
for (int x2 = max(1, i - n); x2 <= min(n, i - 1); x2 ++ )
{
int y1 = i - x1, y2 = i - x2, x = 0;
x += a[x1][y1];
if (x1 != x2) x += a[x2][y2];
f[i][x1][x2] = max({f[i - 1][x1][x2], f[i - 1][x1 - 1][x2],
f[i - 1][x1][x2 - 1], f[i - 1][x1 - 1][x2 - 1]}) + x;
}
cout << f[2 * n][n][n] << '\n';
return 0;
}
[NOIP 2008 提高组] 传纸条
典题了属于是,做过三四次了。
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = N << 1;
int n, m;
int a[N][N];
int f[N][N][N];
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> a[i][j];
for (int i = 2; i <= n + m; i ++ )
for (int x1 = max(1, i - m); x1 <= min(n, i - 1); x1 ++ )
for (int x2 = max(1, i - m); x2 <= min(n, i - 1); x2 ++ )
{
int y1 = i - x1, y2 = i - x2, x = 0;
x += a[x1][y1];
if (x1 != x2) x += a[x2][y2];
f[i][x1][x2] = max({f[i - 1][x1][x2], f[i - 1][x1 - 1][x2],
f[i - 1][x1][x2 - 1], f[i - 1][x1 - 1][x2 - 1]}) + x;
}
cout << f[n + m][n][n] << '\n';
return 0;
}
2.最长上升子序列模型
\(n\) 方的做法比较简单。
优化的可以用树状数组或者二分解决的。
二分思路大概是说,你记录一个数组存当前每个长度的上升子序列的末尾,然后每次找到第一个小于等于 \(ai\) 的替换即可,但是不会证 awa
树状数组的话要看值域,\(f\) 表示结尾为 \(i\) 的最长上升子序列长度,转移显然。
只写了一题意思意思,我也不知道当时为什么没写的说。
[NOIP 2004 提高组] 合唱队形
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = N << 1;
int n, a[N];
int f[N], g[N];
signed main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
f[1] = 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);
reverse(a + 1, a + n + 1);
g[1] = 1;
for (int i = 1; i <= n; i ++ , g[i] = 1)
for (int j = 1; j < i; j ++ )
if (a[j] < a[i]) g[i] = max(g[i], g[j] + 1);
reverse(g + 1, g + n + 1);
int res = 0;
for (int i = 1; i <= n; i ++ )
res = max(res, f[i] + g[i] - 1);
cout << n - res << '\n';
return 0;
}
3.背包模型
分分。
i)简单背包
包括 \(01\),完全,多重小优化,分组,混合
ii)二维背包
比较需要注意的是至少,刚好,不超过的区别。
似乎就转移的时候有点不同,可以分一下,刚好一般都是求方案数,转移和不超过的话差不多。
然后至少得话就是 for (int j = m; j >= v; j -- ) 得变一下。
直接循环到 \(0\),然后转移也要变,变成 \(f[max(0, j - v)]\)
iii)多重背包单调队列优化。
注意到 \(f[i][j]=max(f[i-1][j],f[i-1][j-1*v]+w],f[i-1][j-2*v]+w...)\)
然后 $ f[i][j-1*v]=max(f[i-1][j-v],f[i-1][j-2v]+w...) $
不难发现是求在 \(f[i-1]\) 上的每一个长度为 \(s\) 的滑动窗口内最值,且加上偏移量,故直接单调队列即可。
iv)树上背包
相当于分组背包,对于每一个子树有选取体积为 \(0,1,2...\) 的选择,然后对于每个子树背包即可。
有依赖的背包问题(树形背包 DP)
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int rt, son[N][N], cnt[N];
int f[N][N], v[N], w[N];
void dfs(int u)
{
for (int i = 1; i <= cnt[u]; i ++ )
{
int sn = son[u][i];
dfs(sn);
for (int j = m - v[u]; ~j; j -- )
for (int k = 0; k <= j; k ++ )
f[u][j] = max(f[u][j], f[u][j - k] + f[sn][k]);
}
for (int j = m; j >= v[u]; j -- )
f[u][j] = f[u][j - v[u]] + w[u]; //注意此处应为01背包
for (int j = 0; j < v[u]; j ++ )
f[u][j] = 0;
}
//设f[i][j]表示i子树内选不超过j体积的最大价值
//则可以从i的子节点子树内分别选不同体积,保证最终体积和不超过m-v[u]即可
//完了之后就变成分组背包,和那个机器分配的题一样了
//由此得出一般树形背包问题解法
//对于u考虑子树,然后向上合并,可以转化为背包类型。
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
{
int p;
cin >> v[i] >> w[i] >> p;
if (~p) son[p][ ++ cnt[p]] = i;
else rt = i;
}
dfs(rt);
cout << f[rt][m] << '\n';
return 0;
}
能量石
小 trcik,邻项交换典题。
/*
懒得写了。
无非存一下罢了
考虑先贪心,邻项交换。
然后01背包即可。
大概绿的水平吧。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 10010;
int n;
struct Stone
{
int s, e, l;
}stones[N];
bool cmp(Stone a, Stone b)
{
return a.s * b.l < b.s * a.l;
}
int f[N][M];
int main()
{
int T;
cin >> T;
for (int C = 1; C <= T; C ++ )
{
cin >> n;
int m = 0;
for (int i = 1; i <= n; i ++ )
{
int s, e, l;
cin >> s >> e >> l;
stones[i] = {s, e, l};
m += s;
}
sort(stones + 1, stones + 1 + n, cmp);
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
{
f[i][j] = f[i - 1][j];
if (j >= stones[i].s)
{
int s = stones[i].s, e = stones[i].e, l = stones[i].l;
f[i][j] = max(f[i][j], f[i - 1][j - s] + max(0, e - l * (j - s)));
}
}
int res = 0;
for (int i = 0; i <= m; i ++ ) res = max(res, f[n][i]);
printf("Case #%d: %d\n", C, res);
}
return 0;
}
多重背包问题
比较重头的戏。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1010, M = 20010;
int n, m;
int f[2][M], q[M];
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
{
int v, w, s;
cin >> v >> w >> s;
int hh = 0, tt = -1;
for (int r = 0; r < v; r ++ )
for (int j = r; j <= m; j += v)
{
while (hh <= tt && q[hh] > j - s * v) hh ++ ;
while (hh <= tt && f[(i - 1) & 1][q[tt]] <= f[i & 1][j] - (j - q[tt]) / v * w) tt -- ;
q[tt ++ ] = j;
f[j] = max(f[j], g[q[hh]] + (j - q[hh]) / v * w);
}
}
cout << f[m] << '\n';
return 0;
}
4.状态机模型
相当于说是你对于每一个要考虑的点 \(i\),记录一个状态,表示 \(i\) 当前在你的 \(f\) 中的情况,然后再看一看 \(i-1\) 和 \(i\) 之间的转化关系,一般来说状态机的状态不会很多。
有一种是比较多的,就是让你填一个串,使得一个或多个短串不在这个串中出现,然后这个就是要和 KMP、AC 自动机结合。
对于 KMP,你只要考虑前一位跳完的 \(j\) 指针,再枚举一下 \(i\) 位的字母,然后 \(j\) 跳一下,当 \(j\) 不为短串的 \(m\) 时,更新答案即可。
对于 AC 自动机,就比较麻烦,首先要搞清楚失配指针的含义,是说对于一个点以它为结尾的后缀在以根节点为头的前缀中哪里出现,然后指向前缀末,这样可以使得匹配完当前后缀后,快速匹配下一个。
然后 DP 呢,就是还是枚举每一位,和填的字母,再枚举上一个点的状态,也就是 \(i-1\) 位。考虑对于一个前缀不能出现,是不是就是说它失配指针的逆指向节点也是不合法的,然后转移即可。
设计密码
/*
考虑条件,不包含T是什么意思。
就是说在kmp匹配的时候,j指针不可能跳到m
j的可能状态有0~m,m+1种
设f[i][j]表示填到s的第i位,匹配T时,j的状态为j的方案数
枚举i-1时的j,再用kmp更新到i,即可更新
答案即为f[n]中,状态为0~m-1的方案数之和。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 210, mod = 1e9 + 7;
int n;
string str;
int ne[N], f[N][N];
signed main()
{
cin >> n;
cin >> str;
int sz = str.size();
str = " " + str;
f[0][0] = 1;
for (int i = 2, j = 0; i <= sz; i ++ )
{
while (j && str[j + 1] != str[i]) j = ne[j];
if (str[i] == str[j + 1]) j ++ ;
ne[i] = j;
}
for (int i = 1; i <= n; i ++ )
for (int j = 0; j < sz; j ++ )
for (char k = 'a'; k <= 'z'; k ++ )
{
int u = j;
while (u && str[u + 1] != k) u = ne[u];
if (str[u + 1] == k) u ++ ;
if (u < sz) (f[i][u] += f[i - 1][j]) %= mod;
}
int res = 0;
for (int i = 0; i < sz; i ++ ) (res += f[n][i]) %= mod;
cout << res << '\n';
return 0;
}
[POJ3691/HDU2457]修复DNA
/*
经过KMP状态机DP的洗礼后,做这道题应当是游刃有余了。
类似的,我们考虑设f[i][j]表示填到第i位状态为j的最小修改数。
转移类似。
当一个子串不出现,意味着这个子串的尾端在trie上的节点不能被跳到。
显然,所有和这个前缀相同的后缀尾节点也应标记。
状态转移显然。
最近进行DP的学习,但是发现一些显然的东西看不出来,可能是睡眠缺了。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 110 * 20, M = 10010;
int n, m;
string str;
int tr[N][30], cnt;
int ne[N], st[N], f[N][N];
int get(char ch)
{
if (ch == 'A') return 1;
if (ch == 'T') return 2;
if (ch == 'C') return 3;
return 0;
}
void insert()
{
int p = 0;
for (int i = 0; i < str.size(); i ++ )
{
int c = get(str[i]);
if (!tr[p][c]) tr[p][c] = ++ cnt;
p = tr[p][c];
}
st[p] = 1;
}
void build()
{
queue<int> q;
for (int i = 0; i < 4; i ++ )
if (tr[0][i]) q.push(tr[0][i]);
while (q.size())
{
int t = q.front(); q.pop();
for (int i = 0; i < 4; i ++ )
{
if (!tr[t][i]) tr[t][i] = tr[ne[t]][i];
else
{
ne[tr[t][i]] = tr[ne[t]][i];
q.push(tr[t][i]);
st[tr[t][i]] |= st[ne[tr[t][i]]];
}
}
}
}
signed main()
{
int Case = 0;
while (cin >> n, n)
{
Case ++ ;
memset(tr, 0, sizeof tr);
memset(ne, 0, sizeof ne);
memset(st, 0, sizeof st);
memset(f, 0x3f, sizeof f);
cnt = 0;
for (int i = 1; i <= n; i ++ )
{
cin >> str;
insert();
}
build();
cin >> str;
m = str.size();
str = " " + str;
f[0][0] = 0;
for (int i = 1; i <= m; i ++ )
for (int j = 0; j <= cnt; j ++ )
for (int k = 0; k < 4; k ++ )
{
int w = (get(str[i]) != k), p = tr[j][k];
if (!st[p]) f[i][p] = min(f[i][p], f[i - 1][j] + w);
}
int res = 0x3f3f3f3f;
for (int i = 0; i <= cnt; i ++ )
res = min(res, f[m][i]);
if (res == 0x3f3f3f3f) res = -1;
printf("Case %d: %d\n", Case, res);
}
return 0;
}
5.状态压缩DP
分为两种类型。
一种是棋盘式的,比较好做,处理好相邻两行或者多行的关系,以及每一行合法的填法,转移即可。
另一种是集合式,就是考虑已经处理的集合,需要的话,可以在状态中加入点i,然后根据题目预处理一些再 DP。
还有几道状压 DP,以前写过了,时间不太有,到时候补吧。
最短 Hamilton 路径
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 20, M = 1 << N;
int n;
int a[N][N];
int f[M][N];
signed main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n; j ++ )
cin >> a[i][j];
memset(f, 0x3f, sizeof f);
f[1][0] = 0;
for (int i = 0; i < 1 << n; i ++ )
for (int j = 0; j < n; j ++ )
if (i & (1 << j))
for (int k = 0; k < n; k ++ )
if (i & (1 << k))
f[i][j] = min(f[i][j], f[i - (1 << j)][k] + a[k][j]);
cout << f[(1 << n) - 1][n - 1] << '\n';
return 0;
}
蒙德里安的梦想
/*
随着人的成长,理解能力是越来越强了。
以前看不懂的东西,现在一遍过,还很透彻。
可以发现只要摆放好了横的,剩下的位置可以合法的摆放竖的,就是一种方案。
于是考虑只求横的在合法情况下的摆放方案,与答案等价。
设f[i][j]表示第i列,覆盖了横的的状态为j。
预处理相邻两列可能的状态组合方式,大大降低复杂度。
转移显然。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 13, M = 1 << N;
int n, m;
bool st[M];
int f[N][M];
vector<int> state[M];
signed main()
{
while (cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << n; i ++ )
{
st[i] = true;
int cnt = 0;
for (int j = 0; j < n; j ++ )
{
if (i & (1 << j))
{
if (cnt & 1)
{
st[i] = false;
break;
}
else cnt = 0;
}
else cnt ++ ;
}
if (cnt & 1) st[i] = false;
}
for (int i = 0; i < 1 << n; i ++ )
{
state[i].clear();
for (int j = 0; j < 1 << n; j ++ )
if (!(i & j) && st[i | j])
state[i].push_back(j);
// cout << state[i].size() << ' ';
}
memset(f, 0, sizeof f);
f[1][0] = 1;
for (int i = 2; i <= m + 1; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (auto s : state[j])
f[i][j] += f[i - 1][s];
cout << f[m + 1][0] << '\n';
}
return 0;
}
小国王
/*
这是第三道状压DP了。
根据这三道,我们可以总结出状压一般做法。
分两种:
1.棋盘式,和本题较为相似,是在类似棋盘上放东西,且有限制。
这种直接预处理每个状态可以转移过来的,并试着将每一行拆分考虑。
2.集合式,相当于每一个数选不选,例如最短Hamilton路径。
直接枚举然后转移即可。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 12, M = 1 << 10;
int n, m, st[M], cnt[M];
int f[N][N * N][M];
vector<int> state[M];
bool check(int x)
{
int lst = x % 2, orig = x;
x /= 2;
cnt[orig] = lst;
while (x)
{
if (lst && x % 2) return false;
lst = x % 2;
x /= 2;
cnt[orig] += lst;
}
return true;
}
signed main()
{
cin >> n >> m;
for (int i = 0; i < 1 << n; i ++ )
st[i] = check(i);
for (int i = 0; i < 1 << n; i ++ )
for (int j = i + 1; j < 1 << n; j ++ )
if (st[i] && st[j] && st[i | j] && !(i & j))
state[i].push_back(j), state[j].push_back(i);
state[0].push_back(0);
f[0][0][0] = 1;
for (int i = 1; i <= n + 1; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k < 1 << n; k ++ )
for (int s : state[k])
{
int ct = cnt[k];
if (j >= ct) f[i][j][k] += f[i - 1][j - ct][s];
}
cout << f[n + 1][m][0] << '\n';
return 0;
}
[NOI2001]炮兵阵地
/*
非常的好,啊。
这道题呢,难度评的是蓝题,大概是下位蓝。
状压DP显然。
但是如果你一算,会发现100*1024*1024*1024是炸的。
不过我无所谓,反正赛场的话肯定直接写了。
注意到状压DP一般做法是要对每一行进行处理上一行可以转移过来的状态的。
所以可能去掉一些不合法状态是可以达到时限的。
但是我不会证明。
思路较简单,和小国王、玉米田有得一比。
但是呢,我DP做得还是不多,所以滚动数组优化空间这种赛场上可能要想不到。
最终一句话总结好吧,菜,就多练。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 1 << 10;
int n, m;
int f[2][M][M];
int g[N];
int st[M], cnt[M];
vector<int> state[M];
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
{
char ch;
cin >> ch;
g[i] |= ch == 'H' ? (1 << j - 1) : 0;
}
for (int i = 0; i < 1 << m; i ++ )
{
st[i] = 1;
int i1 = i;
int l1 = i1 % 2; i1 /= 2;
int l2 = i1 % 2; i1 /= 2;
cnt[i] += l1 + l2;
if (l1 && l2) st[i] = 0;
while (i1)
{
if (l1 + l2 + i1 % 2 > 1)
{
st[i] = 0;
break;
}
cnt[i] += i1 % 2;
l1 = l2, l2 = i1 % 2;
i1 /= 2;
}
}
for (int i = 0; i < 1 << m; i ++ )
for (int j = i + 1; j < 1 << m; j ++ )
if (st[i] && st[j] && !(i & j))
state[i].push_back(j), state[j].push_back(i);
state[0].push_back(0);
for (int i = 1; i <= n + 2; i ++ )
for (int j = 0; j < 1 << m; j ++ )
if (!(g[i] & j) && st[j])
for (int k : state[j])
if (!(g[i - 1] & k))
for (int s : state[k])
if (!(g[i - 2] & s) && !(j & s))
f[i & 1][j][k] = max(f[i & 1][j][k], f[(i - 1) & 1][k][s] + cnt[j]);
cout << f[(n + 2) & 1][0][0] << '\n';
return 0;
}
6.区间DP
一般吧。
二维区间 DP 是值得上心的,就是相当于你对子矩阵考虑,然后转移,和一维差不多。
[NOI1999]棋盘分割
一道二维区间 DP 的好题。
哦 no,普通区间 DP 是黄,这题上位绿吧。
/*
感觉luogu评的不合理。
简单区间DP是绿的对吧,那二维不应该是蓝了吗。
说实话这个题很难想,至少对我来说,可能是因为以前没做过二维区间DP。
设f[x1][y1][x2][y2][k]表示对于子矩阵(x1,y1)(x2,y2)切割k次的答案。
转移比较显然,如果你题目看懂了的话。
然后可以用记忆化搜索写,比较简洁。
感觉有一个地方比较怪,就是说没看懂为什么不用枚举两部分切割几刀。
不过根据思路应该是枚举了第一刀切了哪里,然后分的,这样应该是做到了不重不漏的。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 10, M = 15;
int n, m;
double f[N][N][N][N][M];
double s[M][M];
double X;
double calc(int x1, int y1, int x2, int y2)
{
double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] - X;
return sum * sum / m;
}
double dp(int x1, int y1, int x2, int y2, int k)
{
double &v = f[x1][y1][x2][y2][k];
if (v >= 0) return v;
if (!k) return calc(x1, y1, x2, y2);
v = 0x3f3f3f3f;
for (int i = x1; i < x2; i ++ )
{
v = min(v, dp(x1, y1, i, y2, k - 1) + calc(i + 1, y1, x2, y2));
v = min(v, dp(i + 1, y1, x2, y2, k - 1) + calc(x1, y1, i, y2));
}
for (int i = y1; i < y2; i ++ )
{
v = min(v, dp(x1, y1, x2, i, k - 1) + calc(x1, i + 1, x2, y2));
v = min(v, dp(x1, i + 1, x2, y2, k - 1) + calc(x1, y1, x2, i));
}
return v;
}
signed main()
{
n = 8;
cin >> m;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= n; j ++ )
cin >> s[i][j],
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
memset(f, -1, sizeof f);
X = s[n][n] / m;
printf("%.3lf\n", sqrt(dp(1, 1, n, n, m - 1)));
return 0;
}
7.树形DP
注意一下换根 DP 和树形背包,后者在背包类型已提过。
换根 DP 的话,就是要考虑一下你换的时候的一些细节,挺吃空间想象能力的感觉。
树的直径
/*
树形DP裸题,大概黄顶多。
求树的直径,做法比较多,还有bfs,dp可能有些性质啥的。
所以还是要学下。
设f[u][0/1]表示子树中最大次大值,注意不能两个从同一棵子树转移过来。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = N << 1;
int n, res;
int h[N], e[M], ne[M], w[M], idx;
int f[N][2];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
}
void dfs(int u, int fa)
{
f[u][0] = f[u][1] = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u);
if (f[u][0] < f[j][0] + w[i]) f[u][1] = f[u][0], f[u][0] = f[j][0] + w[i];
else if (f[u][1] < f[j][0] + w[i]) f[u][1] = f[j][0] + w[i];
}
res = max(res, f[u][0] + f[u][1]);
}
signed main()
{
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i < n; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs(1, -1);
cout << res << '\n';
return 0;
}
树的中心
/*
经典换根DP。
我发现我的树形DP这块比较薄弱。
可能是之前学的时候没怎么搞懂。
大概是和图论有关的题基本上都不太好,好奇怪。
感觉没有图的题要好做些吧。
设f[u][0/1]表示u点向下的最大次大值,p[u][0/1]为向下经过的点。
g[u]表示u点向上的最大值。
然后换根。
f[u][1]的表述应该不完整,是除了最大值所在子树的最大值。
于是换根显然。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = N << 1;
int n;
int h[N], e[M], w[M], ne[M], idx;
int f[N][2], g[N], p[N][2];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
}
void dfs1(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs1(j, u);
if (f[j][0] + w[i] > f[u][0]) f[u][1] = f[u][0], p[u][1] = p[u][0], f[u][0] = f[j][0] + w[i], p[u][0] = j;
else if (f[j][0] + w[i] > f[u][1]) f[u][1] = f[j][0] + w[i], p[u][1] = j;
}
}
void dfs2(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
if (p[u][0] == j) g[j] = max(g[u], f[u][1]) + w[i];
else g[j] = max(g[u], f[u][0]) + w[i];
dfs2(j, u);
}
}
signed main()
{
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i < n; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs1(1, -1), dfs2(1, -1);
int res = 1e9;
for (int i = 1; i <= n; i ++ ) res = min(res, max(g[i], f[i][0]));
cout << res << '\n';
return 0;
}
二叉苹果树
/*
对树形背包dp的掌握的话,还是有点不够,不能想出。
但是实际上应该是比较明显的了属于是,可能是最近睡眠缺少导致的。
考虑设f[u][j]表示u子树内选j条边,最大答案,其实就是有依赖的背包问题。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = N << 1;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int f[N][N];
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++ ;
}
void dfs(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u);
for (int k = m; ~k; k -- )
for (int l = 0; l < k; l ++ )
f[u][k] = max(f[u][k], f[u][k - 1 - l] + w[i] + f[j][l]);
}
}
signed main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i < n; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
dfs(1, -1);
cout << f[1][m] << '\n';
return 0;
}
数字转换
/*
比较巧妙啊,这道题放在树形DP里做,我可以想得到转化为树。
但是比赛中就不一定了。
每个数约数之和是固定的,所以考虑从约数和连到数。
然后就转化为求树的直径了。
注意到求约数和的时候有个小trick,就是可以把枚举数再枚举约数改成枚举约数再枚举倍数。
O(nsqrt(n)) -> O(nlogn)
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 100010, M = N << 1;
int n;
int h[N], e[M], ne[M], idx;
int sum[N], res, f[N][2];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
if (f[j][0] + 1 > f[u][0]) f[u][1] = f[u][0], f[u][0] = f[j][0] + 1;
else if (f[j][0] + 1 > f[u][1]) f[u][1] = f[j][0] + 1;
}
res = max(res, f[u][0] + f[u][1]);
}
signed main()
{
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i <= n; i ++ )
for (int j = i * 2; j <= n; j += i)
sum[j] += i;
for (int i = 1; i <= n; i ++ )
if (i > sum[i]) add(sum[i], i);
for (int i = 1; i <= n; i ++ )
if (!f[i][0]) dfs(i);
cout << res << '\n';
return 0;
}
皇宫看守
/*
战略游戏的进阶版,也就是我一开始理解错了的题意。
设f[u][0/1/2]表示u分别被父、子、本看守。
比较好想吧应该,就是转移的地方细节处理可能不太好搞。
大概绿。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1510, M = N << 1;
int n;
int h[N], e[M], ne[M], idx;
int w[N], st[N];
int f[N][3];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int fa)
{
int s = 0, s1 = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dfs(j, u);
s += min(f[j][1], f[j][2]);
s1 += min({f[j][0], f[j][1], f[j][2]});
}
f[u][0] = s;
f[u][1] = 0x3f3f3f3f;
f[u][2] = s1 + w[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
f[u][1] = min(f[u][1], s + f[j][2] - min(f[j][1], f[j][2]));
}
}
signed main()
{
memset(h, -1, sizeof h);
cin >> n;
for (int i = 1; i <= n; i ++ )
{
int u;
cin >> u >> w[u];
int m;
cin >> m;
for (int j = 1; j <= m; j ++ )
{
int x;
cin >> x;
add(u, x), add(x, u);
st[x] = 1;
}
}
int rt = 1;
while (st[rt]) rt ++ ;
dfs(rt, -1);
cout << min(f[rt][1], f[rt][2]) << '\n';
return 0;
}
8.数位DP
数位 DP 就还好了,有一定模板。
对于一个区间,你要求出其中所有符合条件的数的什么东西。
然后把它先转化为求区间 \([1,r]-[1,l-1]\),然后,对于右端点拆位考虑,一般是对每一位分两种情况,填 \(0~x-1\) 和 \(x\),\(x\) 就是直接往下做,然后前者一般需要提前 DP 预处理或组合计数。
度的数量
/*
应该是第一次正式学习数位DP。
以前好像学过,忘了。
对于数位DP,一般也有套路,以前一直以为写法不固定。
数位就是指对于每一位考虑。
每一位可能填符合题目要求和不符合两种,分类即可。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int l, r, K, B;
int C[N][N];
void init()
{
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) C[i][j] = 1;
else C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
int f(int x)
{
if (!x) return 0;
vector<int> nums;
while (x) nums.push_back(x % B), x /= B;
int res = 0, lst = 0;
for (int i = nums.size() - 1; ~i; i -- )
{
int n = nums[i];
if (n)
{
res += C[i][K - lst];
if (n > 1)
{
if (K - lst - 1 >= 0) res += C[i][K - lst - 1];
break;
}
else
{
lst ++ ;
if (lst > K) break;
}
}
if (!i && lst == K) res ++ ;
}
return res;
}
signed main()
{
cin >> l >> r >> K >> B;
init();
cout << f(r) - f(l - 1) << '\n';
return 0;
}
数字游戏
/*
好,啊,数位DP,掌握度45%,了解思路后,自己码出了。
预处理f[i][j]i位,最高位为j的乱填数个数。
然后数位DP,左边讨论就是f的乱搞。
大概绿。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int l, r;
int f[N][10];
void init()
{
for (int i = 1; i < 10; i ++ ) f[1][i] = 1;
for (int i = 2; i < N; i ++ )
for (int j = 0; j < 10; j ++ )
for (int k = j; k < 10; k ++ )
f[i][j] += f[i - 1][k];
}
int dp(int x)
{
if (!x) return 0;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0;
for (int i = nums.size() - 1; ~i; i -- )
{
int n = nums[i];
if (n)
{
int bg = i == nums.size() - 1 ? 0 : nums[i + 1];
for (int j = bg; j < n; j ++ )
res += f[i + 1][j];
}
if (i < nums.size() - 1 && nums[i] < nums[i + 1]) break;
if (!i) res ++ ;
}
return res;
}
signed main()
{
init();
while (cin >> l >> r)
cout << dp(r) - dp(l - 1) << '\n';
return 0;
}
不要 62
最唐的一集。
/*
数位DP小成。
粗略判断此题应为下位蓝。
数位DP下位蓝已经难不倒我了,hhh。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int l, r;
int f[N][10];
void init()
{
for (int i = 0; i < 10; i ++ ) f[1][i] = 1;
f[1][4] = 0;
for (int i = 2; i < N; i ++ )
for (int j = 0; j < 10; j ++ )
if (j != 4)
{
for (int k = 0; k < 10; k ++ )
if (j == 6 && k == 2) continue;
else f[i][j] += f[i - 1][k];
}
}
int dp(int x)
{
if (!x) return 1;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0, lst = 0;
for (int i = nums.size() - 1; ~i; i -- )
{
int a = nums[i];
if (a)
{
for (int j = 0; j < a; j ++ )
if (lst == 6 && j == 2) continue;
else res += f[i + 1][j];
}
if (a == 4) break;
if (lst == 6 && a == 2) break;
lst = a;
if (!i) res ++ ;
}
return res;
}
signed main()
{
init();
while (cin >> l >> r, l || r)
cout << dp(r) - dp(l - 1) << '\n';
return 0;
}
数字游戏II
/*
没什么难度啊。
用到一个东西,是以前做过的一道题的套路,好像是和mod101有关的题。
就是说dp状态把取模后的数加进去,然后弄弄。
模板。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 35, M = 110;
int l, r, n;
int f[N][10][M];
void init()
{
memset(f, 0, sizeof f);
for (int i = 0; i < 10; i ++ ) f[1][i][i % n] = 1;
for (int i = 2; i < N; i ++ )
for (int j = 0; j < 10; j ++ )
for (int mod = 0; mod < n; mod ++ )
for (int k = 0; k < 10; k ++ )
f[i][j][mod] += f[i - 1][k][((mod - j) % n + n) % n];
}
int dp(int x)
{
if (!x) return 1;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0, lst_mod = 0;
for (int i = nums.size() - 1; ~i; i -- )
{
int a = nums[i];
if (a)
{
for (int j = 0; j < a; j ++ )
res += f[i + 1][j][(n - lst_mod) % n];
}
(lst_mod += a) %= n;
if (!i && !lst_mod) res ++ ;
}
return res;
}
signed main()
{
while(cin >> l >> r >> n)
{
init();
cout << dp(r) - dp(l - 1) << '\n';
}
return 0;
}
[SCOI2009]Windy 数
/*
这题有点骚的。
如果是考试的话,我还真有可能就栽在这了。
注意点就是考虑前导0,不能直接在DP预处理中更新0的,那样会使算其他的时候算重,不过分两个数组应该可以做。
但还是不做了,直接在数位DP里搞就好了,是只要求最高位不为0。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 35;
int l, r;
int f[N][10];
void init()
{
for (int i = 0; i < 10; i ++ ) f[1][i] = 1;
for (int i = 2; i < N; i ++ )
{
for (int j = 0; j < 10; j ++ )
for (int k = 0; k < 10; k ++ )
if (abs(j - k) >= 2) f[i][j] += f[i - 1][k];
}
}
int dp(int x)
{
if (!x) return 0;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0, lst = -1;
for (int i = nums.size() - 1; ~i; i -- )
{
int n = nums[i];
if (n)
{
for (int j = 0; j < n; j ++ )
if (abs(j - lst) >= 2)
res += f[i + 1][j];
}
if (abs(n - lst) < 2) break;
lst = nums[i];
if (!i) res ++ ;
}
for (int i = 1; i < nums.size(); i ++ )
for (int j = 1; j < 10; j ++ )
res += f[i][j];
return res;
}
signed main()
{
init();
cin >> l >> r;
cout << dp(r) - dp(l - 1) << '\n';
return 0;
}
恨 7 不成妻
应该是有上位蓝的吧?
/*
我去,这题真是唐飞我了。
好,啊,至此,数位DP大成了。
我觉得一般的比较裸的都能切了。
大概这题是上位蓝?
一开始先写了个求个数的,比较唐。
然后发现题目是求平方和,大概过了5min想到正解,因为挺好想的,直接展开一下就好了。
不过这是我认为最坏的方法,于是看了眼题解,对了,但是特别变态。
然后就开始写了,写完调了好久,发现题解和我写法差不多。
调出了两个唐唐小错误,终于AC。
*/
#include <bits/stdc++.h>
#define int long long
#define mod7(x, y) ((x - y) % 7 + 7) % 7
using namespace std;
const int N = 20, M = 7, MOD = 1e9 + 7;
int l, r;
int pw10[N], pw7[N];
int f[N][10][M][M], g[N][10][M][M], h[N][10][M][M];
void init()
{
pw10[0] = 1;
for (int i = 1; i < N; i ++ ) pw10[i] = pw10[i - 1] * 10 % MOD;
pw7[0] = 1;
for (int i = 1; i < N; i ++ ) pw7[i] = pw7[i - 1] * 10 % 7;
for (int i = 0; i < 10; i ++ )
f[1][i][i % 7][i % 7] = i * i % MOD,
g[1][i][i % 7][i % 7] = i % MOD,
h[1][i][i % 7][i % 7] = 1;
f[1][7][0][0] = g[1][7][0][0] = h[1][7][0][0] = 0; //唐唐忘记初始化这个了
for (int i = 2; i < N; i ++ )
for (int j = 0; j < 10; j ++ )
if (j != 7)
{
int a = j % MOD * pw10[i - 1] % MOD;
for (int mod1 = 0; mod1 < 7; mod1 ++ )
for (int mod2 = 0; mod2 < 7; mod2 ++ )
for (int k = 0; k < 10; k ++ )
if (k != 7)
{
int _sum = f[i - 1][k][mod7(mod1, j)][mod7(mod2, pw7[i - 1] * j % 7)];
int sum = g[i - 1][k][mod7(mod1, j)][mod7(mod2, pw7[i - 1] * j % 7)];
int cnt = h[i - 1][k][mod7(mod1, j)][mod7(mod2, pw7[i - 1] * j % 7)];
(f[i][j][mod1][mod2] += (_sum + cnt * a % MOD * a % MOD + 2 * a % MOD * sum % MOD) % MOD) %= MOD;
(g[i][j][mod1][mod2] += (cnt * a % MOD + sum) % MOD) %= MOD;
(h[i][j][mod1][mod2] += cnt) %= MOD;
}
}
}
int dp(int x)
{
if (!x) return 0;
int orig = x % MOD;
vector<int> nums;
while (x) nums.push_back(x % 10), x /= 10;
int res = 0, lst1 = 0, lst2 = 0, lst3 = 0;
for (int i = nums.size() - 1; ~i; i -- )
{
int a = nums[i];
if (a)
{
int cur = lst3 * pw10[i + 1] % MOD;
for (int j = 0; j < a; j ++ )
for (int mod1 = 0; mod1 < 7; mod1 ++ )
for (int mod2 = 0; mod2 < 7; mod2 ++ )
if ((mod1 + lst1) % 7 && (mod2 + lst2 * pw7[i + 1] % 7) % 7) // 我去,忘记乘幂了,唐飞
{
int _sum = f[i + 1][j][mod1][mod2];
int sum = g[i + 1][j][mod1][mod2];
int cnt = h[i + 1][j][mod1][mod2];
(res += (_sum + cnt * cur % MOD * cur % MOD + 2 * cur % MOD * sum % MOD) % MOD) %= MOD;
}
}
if (a == 7) break;
(lst1 += a) %= 7;
(lst2 = lst2 * 10 % 7 + a) %= 7;
(lst3 = lst3 * 10 % MOD + a) %= MOD;
if (!i && lst2 && lst1) (res += orig * orig % MOD) %= MOD;
}
return res;
}
signed main()
{
init();
int T;
cin >> T;
while (T -- )
{
cin >> l >> r;
cout << (dp(r) - dp(l - 1) + MOD) % MOD << '\n';
}
return 0;
}
9.单调队列优化DP
分为两类。
一种是基本没有涉及到 DP 思想的,只是单调队列的扩展。
考虑转化成前缀和之类,然后单调队列维护。
还有二维单调队列,就是先对每一行做,然后把答案存在窗口右端点,再对列做。
另一类是涉及了 DP 的,一般都很显然,难点在线性 DP 设计。
最大子序和
/*
转化成前缀和相减的形式,然后就变单调队列了。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 300010;
int n, m;
int a[N], q[N];
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> a[i], a[i] += a[i - 1];
int hh = 0, tt = 0, res = INT_MIN;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] > m) hh ++ ;
while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
res = max(res, a[i] - a[q[hh]]);
q[ ++ tt] = i;
}
cout << res << '\n';
return 0;
}
烽火传递
/*
唐唐,状态设计只会多维了,单维都不会了,唉。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, m;
int a[N];
int f[N], q[N];
signed main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] > m) hh ++ ;
f[i] = f[q[hh]] + a[i];
while (hh <= tt && f[q[tt]] >= f[i]) tt -- ;
q[ ++ tt] = i;
}
int res = 1e9;
for (int i = n - m + 1; i <= n; i ++ ) res = min(res, f[i]);
cout << res << '\n';
return 0;
}
修剪草坪
好像是一道 USACO。
/*
还是唐,我发现我一维线性DP不太会,奇怪了。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100010;
int n, k;
int a[N], s[N];
int f[N], q[N];
signed main()
{
cin >> n >> k;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] > k) hh ++ ;
f[i] = max(f[i - 1], s[i] + f[q[hh] - 1] - s[q[hh]]);
while (hh <= tt && f[q[tt] - 1] - s[q[tt]] <= f[i - 1] - s[i]) tt -- ;
q[ ++ tt] = i;
}
cout << f[n] << '\n';
return 0;
}
绿色通道
我去,真唐题,英语练习册。
/*
ok啊,加了个二分,结合了烽火传递。
最大最小一眼二分,掌握度感觉主要不足是在线性dp部分。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 50010;
int n, t;
int a[N];
int f[N], q[N];
bool check(int k)
{
memset(f, 0, sizeof f);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] > k + 1) hh ++ ;
f[i] = f[q[hh]] + a[i];
while (hh <= tt && f[q[tt]] >= f[i]) tt -- ;
q[ ++ tt] = i;
}
int res = 1e9;
for (int i = n - k; i <= n; i ++ ) res = min(res, f[i]);
return res <= t;
}
signed main()
{
cin >> n >> t;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
int l = 0, r = n, res = 0;
while (l <= r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid - 1, res = mid;
else l = mid + 1;
}
cout << res << '\n';
return 0;
}
POI2005旅行问题
/*
唐吧这题。
写法不一样写了好久,真唐。
写这个还得很专注,无语。
由此题可以发现这种题需要转化。
一种模型就是转化成前缀和,然后单调队列求最值。
和最大子序和一样。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 3000010;
int n, q[N];
int a[N], b[N];
long long s[N];
bool st[N];
signed main()
{
cin >> n;
for (int i = 1; i <= n; i ++ )
{
scanf("%d%d", a + i, b + i);
a[i + n] = a[i], b[i + n] = b[i];
}
for (int i = 1; i <= n * 2; i ++ )
s[i] = s[i - 1] + a[i] - b[i];
int hh = 0, tt = -1;
for (int i = 1; i <= n * 2; i ++ )
{
if (hh <= tt && i - q[hh] + 1 > n) hh ++ ;
while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
q[ ++ tt] = i;
if (i - n + 1 >= 0 && s[q[hh]] - s[i - n] >= 0) st[i - n + 1] = 1;
}
b[0] = b[n * 2];
for (int i = 1; i <= n * 2; i ++ ) s[i] = s[i - 1] + a[i] - b[i - 1];
hh = 0, tt = -1;
for (int i = n * 2; i; i -- )
{
if (hh <= tt && q[hh] - i + 1 > n) hh ++ ;
while (hh <= tt && s[q[tt]] <= s[i]) tt -- ;
q[ ++ tt] = i;
if (i <= n && s[i + n] - s[q[hh]] >= 0) st[i + n] = 1;
}
for (int i = 1; i <= n; i ++ )
if (!st[i] && !st[i + n]) cout << "NIE\n";
else cout << "TAK\n";
return 0;
}
[HAOI2007]理想的正方形
比较经典,二维单调队列。
/*
果然,单调队列都成黄了,那这题绿也合情合理了,唉。
推广,我的思维不够,先求出每一行,然后向下求出子矩阵,很妙。
最后枚举每个点作为子矩阵的右下角,统计答案。
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 110;
int n, m, k;
int a[N][N], q[N];
int mx[N][N][2], mn[N][N][2];
signed main()
{
cin >> n >> m >> k;
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
cin >> a[i][j];
for (int i = 1; i <= n; i ++ )
{
int hh = 0, tt = -1;
for (int j = 1; j <= m; j ++ )
{
if (hh <= tt && j - q[hh] + 1 > k) hh ++ ;
while (hh <= tt && a[i][q[tt]] <= a[i][j]) tt -- ;
q[ ++ tt] = j;
mx[i][j][0] = a[i][q[hh]];
}
}
for (int i = 1; i <= n; i ++ )
{
int hh = 0, tt = -1;
for (int j = 1; j <= m; j ++ )
{
if (hh <= tt && j - q[hh] + 1 > k) hh ++ ;
while (hh <= tt && a[i][q[tt]] >= a[i][j]) tt -- ;
q[ ++ tt] = j;
mn[i][j][0] = a[i][q[hh]];
}
}
for (int j = 1; j <= m; j ++ )
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] + 1 > k) hh ++ ;
while (hh <= tt && mx[q[tt]][j][0] <= mx[i][j][0]) tt -- ;
q[ ++ tt] = i;
mx[i][j][1] = mx[q[hh]][j][0];
}
}
for (int j = 1; j <= m; j ++ )
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
{
if (hh <= tt && i - q[hh] + 1 > k) hh ++ ;
while (hh <= tt && mn[q[tt]][j][0] >= mn[i][j][0]) tt -- ;
q[ ++ tt] = i;
mn[i][j][1] = mn[q[hh]][j][0];
}
}
int res = 1e9;
for (int i = k; i <= n; i ++ )
for (int j = k; j <= m; j ++ )
res = min(mx[i][j][1] - mn[i][j][1], res);
cout << res << '\n';
return 0;
}
10.斜率优化DP
难点也在线性 DP 设计。
套优化直接根据转移方程,把同号系数尽可能为 \(1\) 并且与前面 \(j\) 相关的移到一边,在求出斜率 \(k\),截距 \(b\),然后 \(k\) 正则维护下凸壳,负则维护上凸壳。
若加入的 \(f\),每次斜率递增,则可以直接更新,弹出前面没用的,优化到 \(O(n)\)。
否则用二分,\(O(nlogn)\)。
[SDOI2012]任务安排(弱化版)
/*
难点依然是在线性DP部分啊。
感觉优化就是个套路问题,直接列出转移方程式,然后移项判断xykb就好了。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 300010;
int n, s;
int t[N], c[N];
int f[N], q[N];
//f[i]=f[j]+(c[i]-c[j])*t[i]+s*(c[n]-c[j])
//f[j]= (t[i]+s) * c[j] + f[i]-c[i]*t[i]-s*c[n]
signed main()
{
cin >> n >> s;
for (int i = 1; i <= n; i ++ ) cin >> t[i] >> c[i];
for (int i = 1; i <= n; i ++ ) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (hh < tt && f[q[hh + 1]] - f[q[hh]] <= (t[i] + s) * (c[q[hh + 1]] - c[q[hh]])) hh ++ ;
int j = q[hh];
f[i] = f[j] + (c[i] - c[j]) * t[i] + s * (c[n] - c[j]);
while (hh < tt && (f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt - 1]]) >= (f[i] - f[q[tt - 1]]) * (c[q[tt]] - c[q[tt - 1]])) tt -- ;
q[ ++ tt] = i;
}
cout << f[n] << '\n';
return 0;
}
[SDOI2012]任务安排
其实就改了个二分,评紫合理。
/*
难点依然是在线性DP部分啊。
感觉优化就是个套路问题,直接列出转移方程式,然后移项判断xykb就好了。
改个二分也是有手就行啊。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 300010;
int n, s;
int t[N], c[N];
int f[N], q[N];
//f[i]=f[j]+(c[i]-c[j])*t[i]+s*(c[n]-c[j])
//f[j]= (t[i]+s) * c[j] + f[i]-c[i]*t[i]-s*c[n]
signed main()
{
cin >> n >> s;
for (int i = 1; i <= n; i ++ ) cin >> t[i] >> c[i];
for (int i = 1; i <= n; i ++ ) t[i] += t[i - 1], c[i] += c[i - 1];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
int l = hh, r = tt - 1, res = hh;
while (l <= r)
{
int mid = l + r >> 1;
if (f[q[mid + 1]] - f[q[mid]] <= (t[i] + s) * (c[q[mid + 1]] - c[q[mid]])) l = res = mid + 1;
else r = mid - 1;
}
int j = q[res];
f[i] = f[j] + (c[i] - c[j]) * t[i] + s * (c[n] - c[j]);
while (hh < tt && (double)(f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt - 1]]) >= (double)(f[i] - f[q[tt - 1]]) * (c[q[tt]] - c[q[tt - 1]])) tt -- ;
q[ ++ tt] = i;
}
cout << f[n] << '\n';
return 0;
}
CD311B Cats Transport
真唐,这题以前 he 过。
/*
果然,优化DP就是基础的线性DP难,上个优化没啥难度。
这题状态设计和转化挺吃操作的。
设f[i][j]表示i个人j只猫被接走的最小等待时间。
然后我们可以求出每只猫刚好接到它的出发时间,就发现每个人接走的猫应该
是按出发时间排序后的一段连续区间。
就可以DP了,说实话,上斜率优化真不难。
其实这个DP想想也还好,因为说你猫分布在每座山上不连续,做着很难受。
然后你尝试DP,发现有些猫的接上刚好可以串在一起,就自然而然考虑去求出发时间。
不难发现每人接走的猫一定是一段区间。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100010, M = 110;
int n, m, p;
int s[N], s1[N], bg[N], t[N], id[N];
int f[M][N], q[N];
signed main()
{
cin >> n >> m >> p;
for (int i = 2; i <= n; i ++ ) cin >> s[i];
for (int i = 1; i <= m; i ++ ) cin >> id[i] >> t[i];
for (int i = 2; i <= n; i ++ ) s[i] += s[i - 1];
for (int i = 1; i <= m; i ++ ) bg[i] = t[i] - s[id[i]];
sort(bg + 1, bg + 1 + m);
for (int i = 1; i <= m; i ++ ) s1[i] = s1[i - 1] + bg[i];
for (int i = 1; i <= m; i ++ ) f[0][i] = 1e18;
for (int i = 1; i <= p; i ++ )
{
int hh = 0, tt = 0;
for (int j = 1; j <= m; j ++ )
{
while (hh < tt && (f[i - 1][q[hh + 1]] + s1[q[hh + 1]] - f[i - 1][q[hh]] - s1[q[hh]]) <= bg[j] * (q[hh + 1] - q[hh])) hh ++ ;
int k = q[hh], b = bg[j];
f[i][j] = (j - k) * bg[j] - s1[j] + s1[k] + f[i - 1][k];
while (hh < tt && (f[i - 1][q[tt]] + s1[q[tt]] - f[i - 1][q[tt - 1]] - s1[q[tt - 1]]) * (j - q[tt - 1]) >= (f[i - 1][j] + s1[j] - f[i - 1][q[tt - 1]] - s1[q[tt - 1]]) * (q[tt] - q[tt - 1])) tt -- ;
q[ ++ tt] = j;
}
}
cout << f[p][m] << '\n';
return 0;
}
[USACO08MAR] Land Acquisition G
典题加练一题。
/*
woc塘坝。
k负的维护上凸壳的嘞,服了。
然后的话问题还是出在线性DP上。
有一个东西没想到,就是说对原序列去重,完了就可以得到r单调递增,c单调递减的序列了。
然后就很简单了。但是我甚至搞到RMQ,这样估计就很难做出来了。
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 50010, M = 17;
int n;
struct Matrix
{
int r, c;
bool operator < (const Matrix &W) const
{
if (r != W.r) return r < W.r;
else return c < W.c;
}
} mat[N];
int q[N], f[N];
signed main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> mat[i].r >> mat[i].c;
sort(mat + 1, mat + 1 + n);
int hh = 1, tt = 1;
for (int i = 2; i <= n; i ++ )
{
while (hh <= tt && mat[tt].c <= mat[i].c) tt -- ;
mat[ ++ tt] = mat[i];
}
n = tt;
hh = 0, tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (hh < tt && f[q[hh + 1]] - f[q[hh]] <= (double)(-mat[i].r) * (mat[q[hh + 1] + 1].c - mat[q[hh] + 1].c)) hh ++ ;
int j = q[hh];
f[i] = f[j] + mat[j + 1].c * mat[i].r;
while (hh < tt && (double)(f[q[tt]] - f[q[tt - 1]]) * (mat[i + 1].c - mat[q[tt - 1] + 1].c) <= (double)(f[i] - f[q[tt - 1]]) * (mat[q[tt] + 1].c - mat[q[tt - 1] + 1].c)) tt -- ;
q[ ++ tt] = i;
}
cout << f[n] << '\n';
return 0;
}
好,至此,DP 提高的结束,我相信我不会这么快就忘了的,写的我电脑都卡了,唉。
不知道为什么,在博客园就不怎么卡了。
本文来自博客园,作者:爱朝比奈まふゆ的MafuyuQWQ。 转载请注明原文链接:https://www.cnblogs.com/MafuyuQWQ/p/18802742

浙公网安备 33010602011771号