DP 选做(长期更新)
在做这个题单:https://www.luogu.com.cn/training/629645
题目按照猎奇程度排序.
CF1016F Road Projects
Hint:抽出 \(1\sim n\) 的链 \(L\) 之后链上每个节点有一棵子树,考虑根据子树的状态分讨,对子树进行处理.
要使最短路最长,但是原先的最短路无法变动. 所以最短路在加一条边发生改变当且仅当出现了一条新的最短路径,并且这条最短路是所有可能新路径中最长的一条.
-
不必构造新的最短路,只需要存在一个子树大小大于 \(2\),连接子树中未直接相连的两点即可.
-
需要构造新的最短路,最短路一定是由两个子树到根距离最长的点相连,且每次询问都连接的是相同的两个点. 设子树 \(u\) 中到 \(u\) 最长距离为 \(f_u\),链上每个点到起点的距离为 \(dis_u\),这些可以简单预处理. 有转移:
拿个变量存一下前缀最大的 \(dis_u+f_u\) 即可做到线性,当然也可以画蛇添足使用单调栈.
询问根据上面的分讨,也可以做到线性. 总复杂度 \(O(n+q)\).
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int maxn = 3e5 + 10;
int n, m;
int tot, head[maxn];
struct edge{int nxt, v, w;} e[maxn << 1];
inline void add(int u, int v, int w) {e[++tot] = {head[u], v, w}, head[u] = tot; return;}
int tp, s[maxn], val[maxn];
bool vis[maxn];
bool dfs1(int u, int fa) {
if(u == n) return true;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w; if(v == fa) continue;
s[++tp] = v, val[tp] = w, vis[v] = true;
if(dfs1(v, u)) return true;
tp--, vis[v] = false;
} return false;
}
int sz[maxn]; ll f[maxn];
void dfs2(int u, int fa) {
sz[u] = 1;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].v, w = e[i].w; if(v == fa || vis[v]) continue;
dfs2(v, u); sz[u] += sz[v], f[u] = max(f[v] + w, f[u]);
}
}
ll ans, dis[maxn]; int r, q[maxn];
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m; bool tag_sz = false;
for(int i = 1, u, v, w; i < n; i++) cin >> u >> v >> w, add(u, v, w), add(v, u, w);
s[tp = 1] = 1, vis[1] = true; dfs1(1, 0);
for(int i = 2; i <= tp; i++) dis[i] = dis[i - 1] + val[i];
for(int i = 1; i <= tp; i++) dfs2(s[i], 0), tag_sz |= (sz[s[i]] > 2);
if(sz[s[1]] > 1 || sz[s[2]] > 1) ans = max(ans, f[s[1]] + f[s[2]] + dis[tp] - dis[2]);
q[r = 1] = 1;
for(int i = 3; i <= tp; i++){
ans = max(ans, f[s[i]] + f[s[q[1]]] + dis[q[1]] + dis[tp] - dis[i]);
if(sz[s[i]] > 1 || sz[s[i - 1]] > 1) ans = max(ans, f[s[i]] + f[s[i - 1]] + dis[tp] - dis[i] + dis[i - 1]);
while(r && f[s[i - 1]] + dis[i - 1] >= f[s[q[r]]] + dis[q[r]]) r--;
q[++r] = i - 1;
}
for(int i = 1; i <= m; i++) {
ll x; cin >> x;
cout << (tag_sz ? dis[tp] : min(dis[tp], ans + x)) << endl;
}
return 0;
}
P3188 [HNOI2007] 梦幻岛宝珠
Hint:拆成题目所给的形式,对容量状压.
题目本身只是一个裸的 \(01\) 背包,但是数据范围奇大无比,所以肯定不能直接做了.
数据范围保证了每个 \(w_i\) 可以拆成 \(a\times 2^b\) 的形式,这启发我们对于每个 \(w_i\) 把 \(a_i\) 和 \(b_i\) 拆出来,考虑用这两个参数来刻画有关 \(w\) 的转移.
考虑状压,设 \(f_{i,j}\) 表示重量为 \(j\times 2^i\) 时的最大价值. 观察到 \(i\) 相同时可以直接转移:
考虑不同的 \(i\) 怎么合并. 如果给 \(i-1\) 分 \(k\times2^{i}\) 的重量,也就是 \((2k)\times2^{i-1}\),相当于 \(j\) 这一维有 \(2k\) 的可分配重量. 同时由于总价值 \(W\) 拆成二进制也可能有 \(2^{i-1}\) 的贡献,如果有也一并加上. 就有转移:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e2 + 10, maxw = 1e3 + 10;
int n, W, w[maxn], v[maxn];
int maxb, wa[maxn], wb[maxn];
ll f[40][maxw];//n个物品容量为a,容量上界为n*a
void solve() {
for(int i = 1; i <= n; i++) cin >> w[i] >> v[i];
for(int i = 1; i <= n; i++) {
for(int b = 1; ; b++) if(w[i] % (1ll << b) != 0) {wb[i] = --b; break;}
wa[i] = w[i] / (1 << wb[i]);
}
for(int i = 1; i <= n; i++)
for(int a = 1000; a >= wa[i]; a--)
f[wb[i]][a] = max(f[wb[i]][a], f[wb[i]][a - wa[i]] + v[i]);
maxb = 0;
for(int s = (W >> 1); s; s >>= 1) maxb++;
for(int b = 1; b <= maxb; b++) {
for(int a = 1000; a >= 0; a--) {
for(int k = 0; k <= a; k++) {
f[b][a] = max(f[b][a], f[b][a - k] + f[b - 1][min(1000, (k << 1) + ((W & (1 << (b - 1))) != 0))]);
}
}
} cout << f[maxb][1] << endl;
return;
}
void cln() {memset(f, 0, sizeof f); return;}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
while(1) {
cin >> n >> W; if(n == -1) break;
solve(), cln();
}
return 0;
}
AT_arc107_d [ARC107D] Number of Multisets
Hint:类似于经典自然根号分拆数的做法,考虑添加一个 \(1\),或者所有数乘 \(1\over 2\) 即可构造出所有可能的可重集.
设 \(f_{i,j}\) 表示当前 \(i\) 个元素和为 \(j\) 的方案数,有转移:
时间复杂度 \(O(n^2)\).右式后者类似于完全背包,可以乘若干个 \(1\over2\),所以第一维是 \(i\).
点击查看代码
#include<bits/stdc++.h>
using namespace std;
constexpr int maxn = 3e3 + 10, mo = 998244353;
int n, k, f[maxn][maxn];
inline int add(const int &x, const int &y) {return x + y >= mo ? x + y - mo : (x + y < 0 ? x + y + mo : x + y);}
inline void upd(int &x, const int &y) {return x = add(x, y), void(0);}
int main() {
cin >> n >> k;
f[0][0] = 1;
for(int i = 1; i <= n; i++) {
for(int j = i; j >= 0; j--) {
if(j * 2 <= i) upd(f[i][j], f[i][j * 2]);
upd(f[i][j], f[i - 1][j - 1]);
}
}
cout << f[n][k];
return 0;
}
题意似乎可以转化为 \(n\) 恰好拆分成 \(k\) 个数的和的方案数,有更加高效的多项式优化做法.
CF1485F Copy or Prefix Sum
Hint:题目第二个条件需要在状态里面记录前缀和,考虑怎么优化转移.
设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,前缀和为 \(j\) 的方案数,有转移:
由于 \(a_i\) 没有限制数据范围,所以一切 \(f_{i-1,x}\) 都可以转移过来.
状态第一维可以压掉;第二维要么相当于整体下标平移,要么是整体求和,所以可以拿个变量存一下.
代码实现开一个 map 来存 \(f\) 的值域维,记一个 sum 表示总和,记一个 res 表示下标偏移总量,整体 DP 即可.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int maxn = 3e3 + 10, mo = 1e9 + 7;
int T, n;
inline int add(const int &x, const int &y) {return x + y >= mo ? x + y - mo : (x + y < 0 ? x + y + mo : x + y);}
inline void upd(int &x, const int &y) {return x = add(x, y), void(0);}
map<ll, int> f;
void solve() {
cin >> n; int x = 0, sum = 0; ll res = 0; map<ll, int>().swap(f);
sum = f[0] = 1;
for(int i = 1; i <= n; i++) {
cin >> x; int val = add(sum, -f[res]);
f[res] = sum; upd(sum, val), res -= x;
} cout << sum << endl;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> T;
while(T--) solve();
return 0;
}
CF1485E Move and Swap
Hint:不难发现每次红蓝都会往下走,考察更加固定的红点,每次交换/不交换分讨,从上一层或父亲转移过来.
-
不交换时,\(u\) 从 \(fa_u\) 转移过来,显然有一个贪心是蓝点最优只可能在这一层的最大或最小值,预处理一下即可.
-
交换后,红点可以任意选择位置,考虑此时蓝点代替原先红点由父亲转移到儿子的最大/最小值,于是同样可以预处理求得. 实现时可以把绝对值去掉维护两个变量.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int maxn = 2e5 + 10, inff = 1e9 + 1;
int T, n, a[maxn];
int tot, head[maxn];
struct edge{int nxt, v;} e[maxn << 1];
inline void add(int u, int v) {e[++tot] = {head[u], v}, head[u] = tot; return;}
int dep[maxn], fa[maxn], mx[maxn], mi[maxn];
vector<int> E[maxn];
void dfs(int u, int f) {
dep[u] = dep[f] + 1, fa[u] = f;
E[dep[u]].push_back(u);
mi[dep[u]] = min(mi[dep[u]], a[u]), mx[dep[u]] = max(mx[dep[u]], a[u]);
for(int i = head[u], v; i; i = e[i].nxt) {
v = e[i].v; if(v == f) continue;
dfs(v, u);
} return;
}
ll ans, f[maxn], mxf[maxn], mif[maxn]; bool vis[maxn];
void solve() {
ans = tot = 0; cin >> n;
for(int i = 1; i <= n; i++) mx[i] = vis[i] = head[i] = f[i] = 0, vector<int>().swap(E[i]), mi[i] = inff;
for(int i = 2, v; i <= n; i++) cin >> v, add(i, v), add(v, i);
for(int i = 2; i <= n; i++) cin >> a[i];
dfs(1, 0);
for(int i = 2; i <= n; i++) {
ll v1 = -inff, v2 = -inff;
for(int u : E[i]) v1 = max(v1, f[fa[u]] + a[u]), v2 = max(v2, f[fa[u]] - a[u]);
for(int u : E[i]) {
f[u] = max(f[u], f[fa[u]] + max(mx[i] - a[u], a[u] - mi[i]));
f[u] = max(f[u], max(v1 - a[u], v2 + a[u]));
ans = max(f[u], ans);
}
}
cout << ans << endl;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> T;
while(T--) solve();
return 0;
}
CF1292C Xenon's Attack on the Gangs
Hint:对贡献反演,拆成至少包含 \(0\sim k-1\) 的路径条数,贪心地填数.
于是考虑贪心地填数使得路径数最大. 容易发现只有所有包含 \(0\) 的路径可能有贡献,所以 \(1\) 应该填在与 \(0\) 相邻的位置,同理 \(2\) 应该填在 \(0,1\) 所在路径的相邻位置. 每次我们的当前路径会向左或向右延伸,造成的贡献就是左右两个子树的 \(size\) 之积. 具体地,对于路径 \(u,v\),贡献实际上是以 \(u\) 为根,子树 \(v\) 的 \(size\) 与以 \(v\) 为根,子树 \(u\) 的 \(size\). 不妨记作 \(size_{rt, i}\),容易在 \(O(n^2)\) 预处理出来,并顺便求出以 \(rt\) 为根的父亲 \(fa_{rt,i}\).
设 \(f_{u,v}\) 为填了链 \((u,v)\) 的最大路径数,不难得到转移:
记忆化搜索即可通过,时间复杂度 \(O(n^2)\).
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e3 + 10;
int n, rt;
int tot, head[maxn];
struct edge{int nxt, v;} e[maxn << 1];
inline void add(int u, int v) {e[++tot] = {head[u], v}, head[u] = tot; return;}
int sz[maxn][maxn], fa[maxn][maxn];
void dfs0(int u, int f) {
fa[rt][u] = f, sz[rt][u] = 1;
for(int i = head[u], v; i; i = e[i].nxt) {
v = e[i].v; if(v == f) continue;
dfs0(v, u); sz[rt][u] += sz[rt][v];
} return;
}
ll ans, f[maxn][maxn];
ll dp(int u, int v) {
if(u == v) return 0;
if(f[u][v]) return f[u][v];
return f[u][v] = max(dp(u, fa[u][v]), dp(fa[v][u], v)) + sz[u][v] * sz[v][u];
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1, u, v; i < n; i++) cin >> u >> v, add(u, v), add(v, u);
for(rt = 1; rt <= n; rt++) dfs0(rt, 0);
for(int u = 1; u <= n; u++) for(int v = 1; v <= n; v++) ans = max(ans, dp(u, v));
cout << ans;
return 0;
}
P6669 [清华集训 2016] 组合数问题
Hint:Locus 定理转换成数位 DP.
似乎在远古校测中见过一样的处理思路. 考虑缩小组合数的范围,用 Locus 定理进行拆分:
递归展开右式,得到的式子就是 \(n,m\) 在 \(k\) 进制下每一位组合数之积. 由于每一项都小于 \(k\) 了,所以成立当且仅当右式为 \(0\),也就是组合数存在一项下面大于上面的. 根据这个性质直接数位 DP 即可.
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
constexpr int mo = 1e9 + 7;
int t; ll k, n, m;
int la, lb;
int a[70], b[70], f[70][2][2][2][2];
inline int add(const int &x, const int &y) {return x + y >= mo ? x + y - mo : x + y;}
inline void upd(int &x, const int &y) {return x = add(x, y), void(0);}
int dp(int t, bool ok, bool dif, bool lima, bool limb) {
if(!t) return ok;
if(f[t][ok][dif][lima][limb] != -1) return f[t][ok][dif][lima][limb];
int res = 0;
int upa = lima ? k - 1 : a[t], upb = limb ? k - 1 : b[t];
for(int i = 0; i <= upa; i++) for(int j = 0; (j <= i || dif) && j <= upb; j++) {
upd(res, dp(t - 1, ok | (i < j), dif | (i != j), lima | (i < upa), limb | (j < upb)));
} return f[t][ok][dif][lima][limb] = res;
}
void init() {la = lb = 0, memset(f, -1, sizeof f), memset(a, 0, sizeof a), memset(b, 0, sizeof b);}
void solve() {
cin >> n >> m; init();
while(n) a[++la] = n % k, n /= k;
while(m) b[++lb] = m % k, m /= k;
cout << dp(max(la, lb), 0, 0, 0, 0) << endl;
return;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> t >> k;
while(t--) solve();
return 0;
}

浙公网安备 33010602011771号