第四周专题(8.1-8.7):动态规划(DP)(14/15)
第四周专题(8.1-8.7):动态规划(DP)
比赛链接:004-2022-08-1
线性DP
E题 最长公共子序列
求出 \(A,B\) 的最长公共子序列长度,并求出该长度的公共串数量。
\(|A|,|B|\leq 5*10^3\)
对于前一个问题,显然有:
对于后者,我们改为计数,判断最长公共子序列究竟是从哪转移过来的。
- \(f_{i,j}=f_{i-1,j-1}+1\) 时,\(g_{i,j}\) 需要加上 \(g_{i-1,j-1}\)
- \(f_{i,j}=f_{i-1,j}\) 时,需要加上 \(g_{i-1,j}\)
- \(f_{i,j}=f_{i,j-1}\) 时,需要加上 \(g_{i,j-1}\)
- 如果同时执行了 \(2,3\),那么根据容斥,还需要减掉 \(g_{i-1,j-1}\)(这一步只需要看 \(f_{i-1,j-1}\) 和 \(f_{i,j}\) 是否相等就行了)。
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e8, N = 5010;
int n, m, f[2][N], r[2][N];
char s1[N], s2[N];
int main()
{
scanf("%s%s", s1 + 1, s2 + 1);
n = strlen(s1 + 1) - 1, m = strlen(s2 + 1) - 1;
for (int k = 0; k <= m; k++)
r[0][k] = 1;
r[1][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
f[i & 1][j] = max(f[(i - 1) & 1][j], f[i & 1][j - 1]);
if (s1[i] == s2[j])
f[i & 1][j] = max(f[i & 1][j], f[(i - 1) & 1][j - 1] + 1);
int k = f[i & 1][j];
r[i & 1][j] = 0;
if (s1[i] == s2[j] && f[(i - 1) & 1][j - 1] == k - 1)
r[i & 1][j] += r[(i - 1) & 1][j - 1];
//
if (f[(i - 1) & 1][j] == k) r[i & 1][j] += r[(i - 1) & 1][j];
if (f[i & 1][j - 1] == k) r[i & 1][j] += r[i & 1][j - 1];
if (f[(i - 1) & 1][j - 1] == k) r[i & 1][j] -= r[(i - 1) & 1][j - 1];
//mod
r[i & 1][j] = (r[i & 1][j] + mod) % mod;
}
}
printf("%d\n%d\n", f[n & 1][m], r[n & 1][m]);
return 0;
}
期望DP
I题 绿豆蛙的归属
给定一张 \(n\) 点 \(m\) 边的(边带权)有向无环图,起点和终点分别为 \(1,n\)。从任何点出发都可以到达终点,且起点可到达任何点。
我们从起点出发,走向终点,当到达一个顶点时,如果该节点有 \(k\) 条出边,那么我们会等概率的随机选择一条。求出起点到达终点的路径总长度的期望。
\(n\leq 10^5,1\leq w_i\leq 10^9\)
记 \(f_i\) 为起点到 \(i\) 的期望长度的话,递推有些不方便,我们不妨换一下,记 \(f_i\) 为点到 \(n\) 的期望长度,那么有 \(f_n=0\),且答案就是 \(f_1\)。
考虑转移方程:点 \(x\) 可能是由其的所有后缀 \(y\) 转移过来且等概率,那么就有:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010, M = N << 1;
//
int tot = 0, ver[M], edge[M], Head[N], Next[M], od[N];
void addEdge(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z;
Next[tot] = Head[x], Head[x] = tot;
}
int n, m, vis[N];
double dp[N];
double dfs(int x) {
if (vis[x]) return dp[x];
vis[x] = 1;
for (int i = Head[x]; i; i = Next[i])
dp[x] += dfs(ver[i]) + edge[i];
return dp[x] /= od[x];
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
int x, y, z;
cin >> x >> y >> z;
addEdge(x, y, z), ++od[x];
}
dp[n] = 0, vis[n] = 1;
printf("%.2lf", dfs(1));
return 0;
}
H题 Tyvj1952 Easy
给定一个 xo 串,该串的得分根据 combo 的总和来计算,每个 combo 都是一个极大连续o串,值为长度的平方。(打个比方,
ooxxxxooooxxx
有两个 combo,分别长度为 2, 4,得分为 \(2^2+4^2=20\))。给定一个由 \(o,x,?\) 组成的串,问号位置随机生成 o 或者 x(等概率,一半一半),求这个串的得分期望。
\(n\leq 3*10^5\)
我们先尝试用 \(g_i\) 表示以 \(s_i\) 结尾的 combo 串的期望长度,那么有
那么我们继续考虑,令 \(f_i\) 表示以 \([1,i]\) 上的串的期望长度。
-
\(s_i=\text{x}\) 时,显然有 \(f_i=f_{i-1}\)
-
\(s_i=\text{o}\) 时,我们可以考虑扩展答案,假定前面的期望 combo长度为 \(k\),那么 \(g_i=k+1\),那么
\[f_i=f_{i-1}-k^2+(k+1)^2=f_{i-1}+2k+1 \] -
\(s_i=\text{?}\) 时,等概率转移,那么得 \(f_i=f_{i-1}+\frac{2k+1}{2}\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 300010;
int n;
char s[N];
double f[N], g[N];
int main() {
scanf("%d%s", &n, s + 1);
for (int i = 1; i <= n; ++i) {
if (s[i] == 'x')
g[i] = 0, f[i] = f[i - 1];
else if (s[i] == 'o')
g[i] = g[i - 1] + 1, f[i] = f[i - 1] + 2 * g[i - 1] + 1;
else {
g[i] = 0.5 * g[i - 1] + 0.5;
f[i] = f[i - 1] + g[i - 1] + 0.5;
}
}
printf("%.4f\n", f[n]);
return 0;
}
B题 奖励关
给定 \(n\) 个宝物,第 \(i\) 个宝物的价值为 \(p_i\)。
接下来连续 \(k\) 次掉落宝物,每次都会等概率的掉落某一种宝物,你需要决定是否选取(采取最优策略),使得最后获得的宝物的价值最大, 求最大值的数学期望。
本题的限制条件如下:
- 每件宝物拥有其独特的选取条件,类似:你在选取该宝物前,必须曾选过了某些宝物
- 一些宝物的价值是负数,但是为了凑齐选取条件,你可能不得不去选一下它
\(k\leq 100,n\leq 15,|p_i|\leq 10^6\)
对于当前选取过的宝物的状态,我们直接状压,用一个 int 存一下就行,范围在 \([0,2^{15})\) 内。(为了方便,我们将所有宝物从 0 开始计数)
我们记 \(dp_{i,j}\) 为当前状态为 \(i\),已经进行了 \(j\) 次选择的条件下,剩下来 \(k-j\) 次所能获得的宝物价值的最大值的数学期望(初始状态下 \(dp_{*,k}=0\))。
每种宝物的掉落是等概率的,所以我们只需要分别算出掉落某种宝物的期望所得值,最后总的除以一下 \(n\) 即可。那么每次掉落宝物 \(t\),我们都有如下选择:
- 不选,那么需要加上 \(dp_{i,j+1}\)
- 选,加上 \(dp_{i|(2^t),j+1}+p_k\)
选不了的时候仅能执行 1,可以选的时候:
- 该物品之前选过了,那么价值大于等于 0 的再选一次,不然不选
- 该物品之前没选过,价值大于等于 0 的直接选,小于 0 的比较一下两者的最大值
总的代码如下:
for (int k = 0; k < n; ++k) {
if ((i | s[k]) != i) {
dp[i][j] += dp[i][j + 1];
continue;
}
if ((i >> k) & 1) {
if (p[k] >= 0) dp[i][j] += dp[i][j + 1] + p[k];
else dp[i][j] += dp[i][j + 1];
}
else {
if (p[k] >= 0) dp[i][j] += dp[i | (1 << k)][j + 1] + p[k];
else dp[i][j] += max(dp[i | (1 << k)][j + 1] + p[k], dp[i][j + 1]);
}
}
不过实际上,我们其实可以把这一大堆分类讨论直接优化掉,仅区分能不能选,是否更优直接交给 max 来处理。
for (int k = 0; k < n; ++k) {
if ((i | s[k]) != i)
dp[i][j] += dp[i][j + 1];
else
dp[i][j] += max(dp[i][j + 1], dp[i | (1 << k)][j + 1] + p[k]);
}
外层的状态数一共有 \(O(k2^n)\) 级别,内部还有 \(O(n)\) 的转移,总复杂度 \(O(nk2^n)\)。
#include<bits/stdc++.h>
using namespace std;
const int K = 110;
int d, n, s[16], p[16];
double dp[1 << 16][K];
int main()
{
//read & init
cin >> d >> n;
for (int i = 0, x; i < n; ++i) {
cin >> p[i];
while (true) {
cin >> x;
if (x == 0) break;
s[i] |= 1 << (x - 1);
}
}
//dp
for (int j = d - 1; j >= 0; j--)
for (int i = 0; i < (1 << n); ++i) {
for (int k = 0; k < n; ++k) {
if ((i | s[k]) != i)
dp[i][j] += dp[i][j + 1];
else
dp[i][j] += max(dp[i][j + 1], dp[i | (1 << k)][j + 1] + p[k]);
}
dp[i][j] /= n;
}
printf("%.6f\n", dp[0][0]);
return 0;
}
数位DP
推荐一手这个博客:数字组成的奥妙——数位dp。
F题 count 数字计数
给定区间 \([l,r]\),求出区间中的所有整数中,每个数码出现了多少次。
\(1\leq l\leq r\leq 10^{12}\)
这题可以 DP 做,但是我觉得分别算贡献要方便一些(我不知道咋说了,直接看代码吧)。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL mul[20], b[20];
LL calc(int H, int L) {
LL res = 0;
for (int i = H; i >= L; i--)
res = res * 10 + b[i];
return res;
}
LL solve0(LL val) {
int tot = 0;
for (LL v = val; v; v /= 10) b[++tot] = v % 10;
LL res = 0;
for (int i = 1; i < tot; ++i) {
if (b[i])
res += (calc(tot, i + 1)) * mul[i - 1];
else
res += (calc(tot, i + 1) - 1) * mul[i - 1] + calc(i - 1, 1) + 1;
}
return res + 1;
}
LL solve(LL val, int x) {
int tot = 0;
for (LL v = val; v; v /= 10) b[++tot] = v % 10;
LL res = 0;
for (int i = 1; i <= tot; ++i) {
if (x < b[i]) res += (calc(tot, i + 1) + 1) * mul[i - 1];
else if (x == b[i])
res += calc(tot, i + 1) * mul[i - 1] + calc(i - 1, 1) + 1;
else
res += calc(tot, i + 1) * mul[i - 1];
}
return res;
}
int main() {
mul[0] = 1;
for (int i = 1; i <= 15; ++i)
mul[i] = mul[i - 1] * 10;
LL L, R;
cin >> L >> R;
cout << solve0(R) - solve0(L - 1);
for (int i = 1; i < 10; ++i)
cout << " " << solve(R, i) - solve(L - 1, i);
return 0;
}
G题 windy 数
对于一个不含前导零,且相邻两个数字之差至少为 2 的正整数被称为 windy 数,求出区间 \([l,r]\) 内有多少 windy 数。(个位数也是 windy 数)
\(1\leq l\leq r \leq 2*10^9\)
一个带了前导零的数位 DP 板子,状态考虑了 \((pos,pre,st)\) 三个(分别代表当前位置,前一个数的值,以及目前状态(是否还在保持 windy 的性质))。采用了记忆化搜索的形式,可以尝试看代码理解一下,不行就去看上面的博客。
#include <bits/stdc++.h>
using namespace std;
const int N = 12;
int bt[N], f[N][10][2];
int dfs(int pos, int pre, int st, int limit, int lead) {
if (pos == 0) return st == 1;
if (!limit && !lead && f[pos][pre][st] != -1) return f[pos][pre][st];
int up = (limit ? bt[pos] : 9), sum = 0;
for (int x = 0; x <= up; ++x) {
int nst = 1, nlead = 0;
if (lead) nlead = x == 0;
else nst = st && abs(x - pre) >= 2;
sum += dfs(pos - 1, x, nst, limit && x == up, nlead);
}
if (!limit && !lead && f[pos][pre][st] == -1)
f[pos][pre][st] = sum;
return sum;
}
int dp(int x) {
memset(f, -1, sizeof(f));
int tot = 0;
for (; x; x /= 10) bt[++tot] = x % 10;
return dfs(tot, 0, 1, 1, 1);
}
int main() {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", dp(r) - dp(l - 1));
return 0;
}
单调队列/斜率优化
本专题需要掌握单调队列和斜率优化两个知识点,建议hxd们先单独研究一下A题题解(指的是洛谷上面的详细斜率优化入门)后在开始看下面题目。
A题 玩具装箱toy
本题的朴素动态规划方程如下:
我们定义 \(s_i=\sum\limits_{k=1}^iC_i\),那么有
定义 \(a_i=s_i+i,b_i=s_i+i+L+1\),那么有
接下来,将其转化为 \(y=kx+b\) 形式:
令 \(y=dp_j+j^2,k=2a_i,b=dp_i-a^2\),可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 50010;
int n, L;
LL sum[N], dp[N];
inline LL a(int i) { return sum[i] + i; }
inline LL b(int i) { return a(i) + L + 1; }
inline LL X(int i) { return b(i); }
inline LL Y(int i) { return dp[i] + b(i) * b(i); }
inline double slope(int i, int j) {
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
deque<int> q;
int main()
{
cin >> n >> L;
for (int i = 1; i <= n; i++) {
cin >> sum[i];
sum[i] += sum[i - 1];
}
q.push_back(0);
int tot = 1;
for (int i = 1; i <= n; i++) {
//delete
while (tot > 1 && slope(q[0], q[1]) < 2 * a(i))
q.pop_front(), tot--;
//dp
dp[i] = dp[q[0]] + (a(i) - b(q[0])) * (a(i) - b(q[0]));
//update
while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
q.pop_back(), tot--;
//insert
q.push_back(i), tot++;
}
cout << dp[n] << endl;
return 0;
}
J题 序列分割
写完上面那些斜率优化后,我们欣喜的发现本题的难度并不在于DP的优化,而是想出朴素方程(qaq)。
我们考虑 \((a,b,c)\) 三个数,如果先分成 \((a,b),(c)\) 然后再分成三个单独段,那么所得值为 \(c(a+b)+ab=ab+ac+bc\),巧合(也不是很巧合)的是,先分成 \((a),(b,c)\) 的话,所得值是 \(a(b+c)+bc=ab+ac+bc\),也是一样的。
我们列出朴素DP方程,记 \(dp_{i,j}\) 为前 \(i\) 个数分成 \(k\) 段所得的最大值,那么有
朴素复杂度是 \(O(n^2k)\),需要消去一维。
形式转换,如下:
\(k\) 单调增,目标是使得 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。
本题有一些注意点:
- 往常,我们会往单调队列里面扔一个初始值 0,但是本题不行(注意到上面的 j 的取值了吗?应该是从 1 开始),所以我们要先扔一个 1 进去,然后从 2 开始慢慢 DP
- 这题卡 STL,所以单调队列得手写(其实没啥不方便的)
- 洛谷上这题得输出方案,所以得注意一下空间的优化(例如 DP 数组就尽量滚动一下)
#include<bits/stdc++.h>
using namespace std;
int read() {
int x = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar());
for (; isdigit(ch); ch = getchar())
x = x * 10 + ch - '0';
return x;
}
typedef long long LL;
const int N = 100010, K = 210;
int n, k, tk, ans[N][K];
LL s[N], dp[N][K];
inline LL X(int i) { return s[i]; }
inline LL Y(int i) { return s[i] * s[i] - dp[i][tk - 1]; }
inline double slope(int i, int j) {
if (X(i) == X(j)) return 1e30;
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
//
int Head, Tail, q[N];
int main()
{
//read
n = read(), k = read();
for (int i = 1; i <= n; i++)
s[i] = read(), s[i] += s[i - 1];
//dp
for (tk = 2; tk <= k + 1; ++tk) {
Head = Tail = 1, q[1] = 1;
for (int i = 2; i <= n; i++) {
//delete
while (Head < Tail && slope(q[Head], q[Head + 1]) < s[i])
++Head;
//dp
dp[i][tk] = dp[q[Head]][tk - 1] + s[q[Head]] * (s[i] - s[q[Head]]);
ans[i][tk] = q[Head];
//update
while (Head < Tail && slope(i, q[Tail - 1]) < slope(q[Tail - 1], q[Tail]))
--Tail;
//insert
q[++Tail] = i;
}
}
cout << dp[n][k + 1] << endl;
//
vector<int> res;
for (int i = n, j = k + 1; j >= 2; j--) {
res.push_back(ans[i][j]);
i = ans[i][j];
}
sort(res.begin(), res.end());
for (int x : res) cout << x << " ";
return 0;
}
K题 小P的牧场
不难推出本题的普通方程:
我们维护一下 \(c_i=ib_i\),随后对 \(\{b_n\},\{c_n\}\) 维护一下前缀和,那么方程就变为了
移项转换,得
可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a[N], b[N], c[N];
LL dp[N];
inline LL X(int i) { return b[i]; }
inline LL Y(int i) { return dp[i] + c[i]; }
inline double slope(int i, int j) {
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
deque<int> q;
int main()
{
//read
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; ++i) cin >> b[i];
//init
for (int i = 1; i <= n; ++i) c[i] = i * b[i];
for (int i = 1; i <= n; ++i)
b[i] += b[i - 1], c[i] += c[i - 1];
//dp
q.push_back(0);
int tot = 1;
for (int i = 1; i <= n; i++) {
//delete
while (tot > 1 && slope(q[0], q[1]) < i)
q.pop_front(), tot--;
//dp
dp[i] = a[i] + dp[q[0]] - c[i] + c[q[0]] + i * (b[i] - b[q[0]]);
//update
while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
q.pop_back(), tot--;
//insert
q.push_back(i), tot++;
}
cout << dp[n] << endl;
return 0;
}
L题 防御准备
本题就是上题中 \(b_i=1\) 的子情况,直接改一下就可以了。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a[N], c[N];
LL dp[N];
inline LL X(int i) { return i; }
inline LL Y(int i) { return dp[i] + c[i]; }
inline double slope(int i, int j) {
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
deque<int> q;
int main()
{
//read
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
//init
for (int i = 1; i <= n; ++i) c[i] = i;
for (int i = 1; i <= n; ++i)
c[i] += c[i - 1];
//dp
q.push_back(0);
int tot = 1;
for (int i = 1; i <= n; i++) {
//delete
while (tot > 1 && slope(q[0], q[1]) < i)
q.pop_front(), tot--;
//dp
dp[i] = a[i] + dp[q[0]] - c[i] + c[q[0]] + 1LL * i * (i - q[0]);
//update
while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
q.pop_back(), tot--;
//insert
q.push_back(i), tot++;
}
cout << dp[n] << endl;
return 0;
}
M题 仓库建设
不难推出本题的普通方程:
我们维护一下 \(a_i=x_ip_i,b_i=p_i\),随后对 \(\{a_n\},\{b_n\}\) 维护一下前缀和,那么方程就变为了
移项转换,得
可知 \(k\) 单调增,目标是让 \(b\) 最小化,那么用单调队列维护一个下凸壳即可。
这题在洛谷上面的 subtask(Hack数据)有点怪,我不好说了:
- 若 \(p_i=0\) 恒成立,那么说明没有物资,也就不需要建立仓库
- 有时候末尾几处并没有物资,也就是说并非一定得在位置 n 处建立仓库。我们求出最后一个有物资的位置是 \(t\),那么答案就是 \(\min\limits_{t\leq i\leq n}dp_i\)。(为啥不是 \(dp_t\)?因为有时候后面建立仓库的价格特别小,可以抵消路途的路费)
- 这题算斜率的时候需要特意注意一下 \(dx=0\) 的情况,此时需要返回无穷大/无穷小
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL x[N], p[N], c[N];
LL a[N], b[N];
LL dp[N];
inline LL X(int i) { return b[i]; }
inline LL Y(int i) { return dp[i] + a[i]; }
inline double slope(int i, int j) {
if (X(i) == X(j)) return 1e20;
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
deque<int> q;
int main()
{
//read
cin >> n;
for (int i = 1; i <= n; i++)
cin >> x[i] >> p[i] >> c[i];
//init
for (int i = 1; i <= n; ++i) a[i] = x[i] * p[i];
for (int i = 1; i <= n; ++i) b[i] = p[i];
for (int i = 1; i <= n; ++i)
a[i] += a[i - 1], b[i] += b[i - 1];
//dp
q.push_back(0);
int tot = 1;
for (int i = 1; i <= n; i++) {
//delete
while (tot > 1 && slope(q[0], q[1]) < x[i])
q.pop_front(), tot--;
//dp
dp[i] = c[i] + dp[q[0]] + x[i] * (b[i] - b[q[0]]) - (a[i] - a[q[0]]);
//update
while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
q.pop_back(), tot--;
//insert
q.push_back(i), tot++;
}
int t = 0;
for (int i = n; i >= 1; i--)
if (p[i]) { t = i; break; }
LL ans = 1e18;
for (int i = t; i <= n; ++i)
ans = min(ans, dp[i]);
cout << ans << endl;
return 0;
}
N题 特别行动队
先预处理一下前缀和,\(s_i=\sum\limits_{k=1}^ix_i\)。
本题的朴素动态规划转移方程为:
移项更换,得
可知 \(k\) 单调增,目标是使得 \(b\) 最小化(即 \(dp_i\) 最大化),那么用单调队列维护一个下凸壳即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
int n;
LL a, b, c, s[N], dp[N];
inline LL X(int i) {
return s[i];
}
inline LL Y(int i) {
return -dp[i] - a * s[i] * s[i] + b * s[i];
}
inline double slope(int i, int j) {
double dy = Y(i) - Y(j), dx = X(i) - X(j);
return dy / dx;
}
deque<int> q;
int main()
{
cin >> n >> a >> b >> c;
for (int i = 1; i <= n; i++) {
cin >> s[i];
s[i] += s[i - 1];
}
q.push_back(0);
int tot = 1;
for (int i = 1; i <= n; i++) {
//delete
while (tot > 1 && slope(q[0], q[1]) < -2 * a * s[i])
q.pop_front(), tot--;
//dp
dp[i] = dp[q[0]] + a * (s[i] - s[q[0]]) * (s[i] - s[q[0]]) + b * (s[i] - s[q[0]]) + c;
//update
while (tot > 1 && slope(i, q[tot - 2]) < slope(q[tot - 2], q[tot - 1]))
q.pop_back(), tot--;
//insert
q.push_back(i), tot++;
}
cout << dp[n] << endl;
return 0;
}
杂项
C题 栅栏(搜索,剪枝)
给定 \(m\) 块木棒,第 \(i\) 根的长度为 \(a_i\)。
现在我们想要得到 \(n\) 根木棒,且第 \(i\) 根的长度为 \(b_i\),我们可以将自己有的木棒裁剪一下(例如一根长度 10 的木棒,可以剪成一根 8 和 一根 2)以尽量达成目标。
问,我们至多可以得到多少根目标木棒?
\(m\leq 50,n\leq 10^3,1\leq a_i,b_i\leq 2^{15}\)
这个最大木板数量可以二分,而选择的时候也一定是优先选那些短的目标木板。那么,问题就变成了:能否用当前这些木板裁剪出我们需要的木板?
讲真,好长时间不写爆搜了(高三不谈,大学两年ACM因为必须写正解,所以也不咋去骗分),导致我一开始没写得出这个基础 DFS,好一会才想起来咋枚举:从前到后,依次检查第 \(i\) 块目标木棒选定了哪一个已有木棒,减去一下后继续搜,直到所有都达成。
bool dfs(int d) {
if (d == 0) return true;
for (int i = 1, f; i <= m; ++i)
if (a[i] >= b[d]) {
a[i] -= b[d], f = dfs(d - 1), a[i] += b[d];
if (f) return true;
}
return false;
}
但毋庸置疑,这个爆搜不用刻意卡都会 T,所以我们看看咋优化:
-
可行性剪枝
如果当前剩下的木棒长度小于目标木棒的剩余长度,直接返回 false
-
搜索树优化
- 先处理大的,后处理小的(如果成不了,让他在搜索树的前几层就断掉,而不是一直拖到后面)
- 如果两个相邻目标木棒长度相同,假设前者选择了现有木棒 \(i\),那么后者枚举的时候也从 \(i\) 开始,而非从 1 开始(两种行为本质上是等价的,所以可以强制要求下一次枚举的时候位置大于上一次)
#include<bits/stdc++.h>
using namespace std;
const int M = 60, N = 1010;
int m, n, a[M], b[N], s[N];
bool dfs(int d, int left, int pos) {
if (s[d] > left) return false;
if (d == 0) return true;
for (int i = pos; i <= m; ++i)
if (a[i] >= b[d]) {
a[i] -= b[d];
int f = dfs(d - 1, left - b[d], b[d] == b[d - 1] ? i : 1);
a[i] += b[d];
if (f) return true;
}
return false;
}
int main()
{
int sum = 0;
//read
cin >> m;
for (int i = 1; i <= m; ++i)
cin >> a[i], sum += a[i];
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> b[i];
sort(b + 1, b + n + 1);
for (int i = 1; i <= n; ++i)
s[i] = s[i - 1] + b[i];
//Binary Search
int l = 0, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (dfs(mid, sum, 1)) l = mid;
else r = mid - 1;
}
cout << l << endl;
return 0;
}
D题 牛跑步(K短路,A*)
给定一张 \(n\) 点 \(m\) 边的边带权有向图,求出起点到终点的 最短路,次短路,......,k 短路的值。
\(1\leq n\leq 10^3,1\leq m\leq 10^4,k\leq 100,1\leq w_i\leq 10^6\)
K短路板子题,我们先求出最短路径,然后用这个作为启发函数,从而来跑A*。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
//test2
const int N = 1010, M = 10010;
int n, m, k;
struct Graph {
int tot = 0, ver[M], edge[M], Head[N], Next[M];
void addEdge(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z;
Next[tot] = Head[x], Head[x] = tot;
}
} G1, G2;
//First-Dijkstra
struct Node1 {
int x; LL d;
bool operator < (const Node1 &rhs) const {
return d > rhs.d;
}
};
LL g[N];
void FirstDijkstra() {
priority_queue<Node1> q;
memset(g, 0x3f, sizeof(g));
g[1] = 0, q.push((Node1){1, 0});
while (!q.empty()) {
Node1 now = q.top(); q.pop();
int x = now.x;
if (g[x] != now.d) continue;
for (int i = G2.Head[x]; i; i = G2.Next[i]) {
int y = G2.ver[i], z = G2.edge[i];
if (g[y] > g[x] + z) {
g[y] = g[x] + z;
q.push((Node1){y, g[y]});
}
}
}
}
//Astar
struct Node2 {
int x; LL d;
bool operator < (const Node2 &rhs) const {
return d + g[x] > rhs.d + g[rhs.x];
}
};
void Astar() {
priority_queue<Node2> q;
q.push((Node2){n, 0});
int cnt = 0;
while (!q.empty()) {
Node2 now = q.top(); q.pop();
int x = now.x; LL d = now.d;
if (x == 1) {
printf("%lld\n", d);
if (++cnt == k) break;
}
for (int i = G1.Head[x]; i; i = G1.Next[i]) {
int y = G1.ver[i], z = G1.edge[i];
q.push((Node2){y, d + z});
}
}
while (cnt++ < k) puts("-1");
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < m; ++i) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
G1.addEdge(x, y, z), G2.addEdge(y, x, z);
}
FirstDijkstra();
Astar();
return 0;
}