小清新dp题总结
P7831 [CCO 2021] Travelling Merchant
题解:
要想一直走下去,就肯定要走到环上,
那些走不到环上的点答案就必定为 \(-1\)。
剩下的点便一定可以通过一条路径去到某个环上。
设 \(f_u\) 为答案,
我们很容易得到 \(f_u=min(\max(r_i,f_v-p_i) (u,v) \in e_i)\) 这样一个转移方程。
但是由于图上有环,我们很难转移。
我们考虑对转移钦定一个顺序使其可以有序的转移。
不难发现,当初始资产为 \(max(r_i)\) 时,是一定可以不停地行走的。
因此我们可以将所有边按 \(r_i\) 从大到小考虑。
那么我们现在就有了一个保证存在答案的点,从这个点去做一个类似于拓扑排序的东西即可。
具体的,按 \(r_i\) 从大到小删边,边删边边删掉出度为 \(0\) 的点。
由于 \(dp\) 是从终点转移到起点,因此需要建个反图。
Code:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 2e5 + 7, M = 2e5 + 7;
const ll inf = 1e15;
int n, m;
int out[N];
int q[N], hh = 1, tt;
bool vis[N];
ll f[N];
struct node{
int x, y;
ll r, p;
}g[M];
struct Edge{
struct edge{
int to;
ll r, p;
int pre;
}e[M];
int head[N], tot;
void add(int x, int y, ll r, ll p){
e[++tot] = {y, r, p, head[x]};
head[x] = tot;
}
}E;
bool cmp1(node a, node b){
return a.r > b.r;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++){
int x, y;
ll r, p;
cin >> x >> y >> r >> p;
g[i] = {x, y, r, p};
out[x]++;
E.add(y, x, r, p);
}
sort(g + 1, g + m + 1, cmp1);
for(int i = 1; i <= n; i++){
if(!out[i]) q[++tt] = i;
f[i] = inf;
}
for(int i = 1; i <= m; i++){
while(hh <= tt){
int u = q[hh++];
for(int j = E.head[u]; j; j = E.e[j].pre){
if(vis[j]) continue;
vis[j] = 1;
int v = E.e[j].to;
out[v]--;
if(!out[v]) q[++tt] = v;
if(f[u] != inf) f[v] = min(f[v], max(E.e[j].r, f[u] - E.e[j].p));
}
}
if(!vis[i]){
vis[i] = 1;
int u = g[i].x;
out[u]--;
if(!out[u]) q[++tt] = u;
f[u] = min(f[u], g[i].r);
}
}
for(int i = 1; i <= n; i++) printf("%lld ", (f[i] == inf) ? -1 : f[i]);
return 0;
}
B4280 [蓝桥杯青少年组国赛 2023] 数学实验
题解:
特别清新的一道题。
不难发现最终答案上限仅为 \(98\),因此考虑将值域包含进状态中。
不难发现这样可以做一个类似于存在性 \(dp\) 的做法。
设 \(f_{k,i}\) 为 \(i\) 合成出 \(k\) 的最小右端点。
转移即为 \(f_{k,i}=f_{k-1,f_{k-1,i}}\)。
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 107;
int n;
int a[N];
int f[107][N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
f[a[i]][i] = i + 1;
}
int ans = 0;
for(int k = 1; k <= 98; k++){
for(int i = 1; i <= n; i++){
if(!f[k][i]) f[k][i] = f[k - 1][f[k - 1][i]];
if(f[k][i]) ans = k;
}
}
printf("%d\n", ans);
return 0;
}
P11255 [GDKOI2023 普及组] 淋雨
题解:
一道数据结构优化 \(dp\) 的好题。
不难发现小数会影响我们的计算,遂将时间放慢 \(v_g\) 倍,不难证明这样操作不会影响题目的正确性。
对于题目给出的多组询问,我们可以加入一些虚拟水滴 \((s_i,0)\),同时设状态 \(f_i\) 为从第 \(i\) 个水珠出发的答案,最后只须将虚拟水滴的答案 \(-1\) 即可。
这里设第 \(i\) 个水珠的坠落位置为 \(p_i\),坠落时间为 \(t_i\),那么转移方程即为
我们很容易想到 \(O(n^2)\) 的暴力转移,但这样时间复杂度太高了,考虑优化。
考虑将这个条件式子的绝对值拆掉,得到 \(\max(p_i-p_j,p_j-p_i) \leq v_c(t_j-t_i)\)。
由于左边式子的较大值符合条件,那么较小值也符合条件,便得到
整理可得
不难发现这就是个二维偏序问题,树状数组解决即可。
Code:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 7;
int n, q;
ll v_g, v_w, v_c;
int s[N];
ll lis[N << 1];
int f[N << 1];
int ans[N];
map<int, int>mp;
struct rain{
ll t, p;
}a[N << 1];
struct node{
ll x, y;
int id;
}p[N << 1];
struct BIT{
int c[N << 1];
int lowbit(int x){
return x & (-x);
}
void add(int x, int d){
for(int i = x; i <= n + q; i += lowbit(i)){
c[i] = max(c[i], d);
}
}
int query(int x){
int s = 0;
for(int i = x; i; i -= lowbit(i)){
s = max(s, c[i]);
}
return s;
}
}T;
bool cmp1(node a, node b){
if(a.x != b.x) return a.x < b.x;
else return a.y < b.y;
}
void lisan_y(){
for(int i = 1; i <= n + q; i++){
lis[i] = p[i].y;
}
sort(lis + 1, lis + n + q + 1);
int len = unique(lis + 1, lis + n + q + 1) - lis - 1;
for(int i = 1; i <= n + q; i++){
p[i].y = lower_bound(lis + 1, lis + len + 1, p[i].y) - lis;
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> q >> v_g >> v_w >> v_c;
for(int i = 1; i <= n; i++){
ll x, y;
cin >> x >> y;
a[i] = {y, x * v_g + y * v_w};
}
int t = 0;
for(int i = 1; i <= q; i++){
cin >> s[i];
if(mp[s[i]]) continue;
else mp[s[i]] = ++t, a[n + t] = {0, s[i] * v_g};
}
for(int i = 1; i <= n + q; i++){
p[i] = {a[i].p - v_c * a[i].t, -v_c * a[i].t - a[i].p, i};
}
lisan_y();
sort(p + 1, p + n + q + 1, cmp1);
for(int i = 1; i <= n + q; i++){
f[i] = T.query(p[i].y) + 1;
T.add(p[i].y, f[i]);
if(p[i].id > n) ans[p[i].id - n] = f[i];
}
for(int i = 1; i <= q; i++) printf("%d\n", ans[mp[s[i]]] - 1);
return 0;
}
P10784 【MX-J1-T4】『FLA - III』Wrestle
题解:
考虑只对蓝色线段作 \(dp\)。
若把每条蓝色线段所相交的红色线段权值之和视作代价,相交点数之和视作价值,就可以将其转化为一个经典的 \(01\) 背包问题。
但还有“题目给定的每条红色线段至多与你选择的 \(1\) 条蓝色线段有交集。”这一条限制。
由于题目给出这些线段的优秀性质,不难发现一条蓝色线段所无法共存的蓝色线段一定是在这一条线段之前的一个区间。
那么我们便可双指针预处理出最后一条能共存的线段 \(pre_i\),代价 \(w_i\) 和价值 \(v_i\)。
设 \(f_{i,j}\) 为考虑到第 \(i\) 条蓝色线段,代价总和为 \(j\) 的最大价值。
转移方程即为:
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, M = 5007;
int n, m, k;
int pl[M], pr[M];
int w[M], v[M], pre[M];
int f[M][M];
struct line{
int l, r, w;
}a[N], b[M];
bool cmp1(line a, line b){
return a.r < b.r;
}
bool check(line a, line b){
return min(a.r, b.r) >= max(a.l, b.l);
}
int solve(line a, line b){
return min(a.r, b.r) - max(a.l, b.l) + 1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> k;
for(int i = 1; i <= n; i++){
cin >> a[i].l >> a[i].r >> a[i].w;
}
for(int i = 1; i <= m; i++){
cin >> b[i].l >> b[i].r;
}
sort(a + 1, a + n + 1, cmp1);
sort(b + 1, b + m + 1, cmp1);
for(int i = 1, j = 1; i <= m; i++){
while(j <= n && a[j].r < b[i].l) j++;
pl[i] = j;
while(j <= n && check(a[j], b[i])){
w[i] += a[j].w;
v[i] += solve(a[j], b[i]);
j++;
}
pr[i] = --j;
}
for(int i = 1; i <= m; i++){
for(int j = i - 1; j; j--){
if(pr[j] < pl[i]){
pre[i] = j;
break;
}
}
}
for(int i = 1; i <= m; i++){//dp
for(int j = 0; j <= k; j++){
if(j < w[i]) f[i][j] = f[i - 1][j];
else f[i][j] = max(f[i - 1][j], f[pre[i]][j - w[i]] + v[i]);
}
}
printf("%d\n", f[m][k]);
return 0;
}
P4649 [IOI 2007] training 训练路径
题解:
一道状压 dp 和树形 dp 结合的好题。
删边的操作显然是比较不好处理的,我们考虑将其转换为求最大残留边权,答案即为总边权减去最大添加边权。
对于所有非树边,如果添加其所得到的环是偶环,便可以将其直接删去,不考虑到后续的讨论范围中。
考虑两个共顶点的奇环,不难发现这两个环可以合成一个偶环,因此要求不能存在两个奇环共顶点。
这就要求最终的图为一个仅存在奇环的仙人掌图。
由于每个点的度数不超过 \(10\),状压儿子是否被考虑。
设 \(f_{i,S}\) 为以 \(i\) 为根的子树中不考虑儿子集合 \(S\) 的最大残留边权。
若不考虑非树边,则
对于每一条连接 \(u,v\) 的非树边:
设 \(t=\operatorname{lca}(u,v)\)。
此时 \(u \rightarrow t \rightarrow v \rightarrow u\) 形成了一个环。
由于不能存在两个环共顶点,因此在这个环上的点强制其不被考虑。
所以对于所有在环上的点,其父亲的贡献都为 \(f_{fa_u,u}\) 而非 \(f_{fa_u,0}\)。
设 \(u,v\) 分别在 \(t\) 的 \(x,y\) 子树上。
所以转移方程便为
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 1007, M = 5007, U = 1 << 10;
int n, m;
int lg[N], st[N][17], idx[N], dfn[N], cnt;
int dep[N], son[N], id[N], fa[N];
int f[N][U + 7];
struct Node{
int x, y, w;
};
vector<Node>g[N];
struct Edge{
int head[N], tot = 1;
int dfn[N], idx[N], cnt;
bool vis[M << 1];
struct edge{
int to, w, pre;
}e[M << 1];
void add(int x, int y, int z){
e[++tot] = {y, z, head[x]};
head[x] = tot;
}
void build_ST(){
for(int j = 1; (1 << j) <= n; j++){
for(int i = 1; i + (1 << j) - 1 <= n; i++){
st[i][j] = min(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
}
}
}
int lca(int x, int y){
if(x == y) return x;
x = dfn[x];
y = dfn[y];
if(x > y) swap(x, y);
int d = lg[y - x];
return idx[min(st[x + 1][d], st[y - (1 << d) + 1][d])];
}
void dfs1(int u, int father){
dfn[u] = ++cnt;
idx[cnt] = u;
st[cnt][0] = dfn[father];
fa[u] = father;
for(int i = head[u]; i; i = e[i].pre){
int v = e[i].to;
if(v == fa[u] || e[i].w) continue;
dep[v] = dep[u] + 1;
id[v] = son[u]++;
dfs1(v, u);
}
}
void doit(){
for(int u = 1; u <= n; u++){
for(int i = head[u]; i; i = e[i].pre){
int v = e[i].to;
if(v == fa[u] || !e[i].w || vis[i]) continue;
vis[i] = vis[i ^ 1] = 1;
if((dep[u] + dep[v]) % 2 == 0){
g[lca(u, v)].push_back(Node{u, v, e[i].w});
}
}
}
}
void dp(int u){
for(int i = head[u]; i; i = e[i].pre){
int v = e[i].to;
if(v == fa[u] || e[i].w) continue;
dp(v);
for(int S = 0; S < U; S++){
if(!(S & (1 << id[v]))) f[u][S] += f[v][0];
}
}
for(Node tmp : g[u]){
int x = tmp.x, y = tmp.y, w = tmp.w;
if(x != u){
w += f[x][0];
while(fa[x] != u){
w += f[fa[x]][1 << id[x]];
x = fa[x];
}
}
else x = -1;
if(y != u){
w += f[y][0];
while(fa[y] != u){
w += f[fa[y]][1 << id[y]];
y = fa[y];
}
}
else y = -1;
int T = 0;
if(x != -1) T |= (1 << id[x]);
if(y != -1) T |= (1 << id[y]);
for(int S = 0; S < U; S++){
if((S & T) == 0) f[u][S] = max(f[u][S], f[u][S | T] + w);
}
}
}
}E;
void init(){
for(int i = 2; i <= N - 7; i++) lg[i] = lg[i >> 1] + 1;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
init();
int sum = 0;
for(int i = 1; i <= m; i++){
int x, y, z;
cin >> x >> y >> z;
E.add(x, y, z);
E.add(y, x, z);
sum += z;
}
memset(st, 0x3f, sizeof(st));
E.dfs1(1, 0);
E.build_ST();
E.doit();
E.dp(1);
printf("%d\n", sum - f[1][0]);
return 0;
}
P3959 [NOIP 2017 提高组] 宝藏
题解:
因为 \(n \leq 12\),因此可以考虑设计与 \(n\) 相关的指数级算法。
不难发现最终形成的图会是一棵有根树,因此考虑枚举根节点做 dp。
由于一条边的贡献和深度有关,因此设 \(f_{i,S}\) 为深度前 \(i\) 层的节点的集合为 \(S\) 的最小代价。
转移便可以考虑从 \(i-1\) 层的状态转移过来,即:
其中 \(c_{T,S}\) 表示 \(T\) 通过合并变成 \(S\) 的最小代价。
转移复杂度 \(O(n^2 3^n)\),预处理复杂度 \(O(n^2 3^n)\)。
Code:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 17, U = 1 << 12;
const ll inf = 0x3f3f3f3f;
int n, m;
ll w[N][N], c[U][U];
ll f[N][U];
ll ans = inf;
void DP(int x){
for(int i = 0; i <= n; i++){
for(int S = 0; S < U; S++) f[i][S] = inf;
}
f[0][1 << (x - 1)] = 0;
ll sum = inf;
for(int i = 1; i < n; i++){
for(int S = 1; S < U; S++){
for(int T = S; T; T = (T - 1) & S){
if(S == T) continue;
f[i][S] = min(f[i][S], f[i - 1][T] + i * c[T][S]);
}
if(S == U - 1) sum = min(sum, f[i][S]);
}
}
ans = min(ans, sum);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
memset(w, 0x3f, sizeof(w));
cin >> n >> m;
if(n == 1){
printf("0\n");
return 0;
}
for(int i = 1; i <= m; i++){
int x, y;
ll z;
cin >> x >> y >> z;
w[x][y] = w[y][x] = min(w[x][y], z);
}
for(int S = 1; S < U; S++){
for(int T = S; T; T = (T - 1) & S){
if(S == T) continue;
for(int i = 1; i <= n; i++){
if((S & (1 << (i - 1))) ^ (T & (1 << (i - 1)))){
ll k = inf;
for(int j = 1; j <= n; j++){
if(T & (1 << (j - 1))) k = min(k, w[i][j]);
}
c[T][S] += k;
}
}
}
}
for(int i = 1; i <= n; i++){
DP(i);
}
printf("%lld\n", ans);
return 0;
}
P1295 [TJOI2011] 书架
优美的 \(O(n)\) 做法。
不难想到设 \(f_i\) 为考虑到 \(i\) 的最小代价。
转移方程即为
这个式子暴力转移是 \(O(n^2)\) 的,考虑优化。
直接上线段树当然是可以的,但没那么优雅,这里带来一个 Trick:单调栈优化单调队列优化 dp。
首先考虑两个点 \(j\) 和 \(j'\),如果 \(j < j'\) 且 \(\max_{k \in (j, i]} a_k = \max_{k \in (j', i]} a_k\),那么 \(j\) 一定优于 \(j'\),这是因为 \(f_i\) 单调不降的缘故,所以 \(f_{j} \leq f_{j'}\)。
那么我们便可以维护一个单调队列,对于队列中的每个点 \(q_i\),都有 \(a_{q_1} > a_{q_2} > a_{q_3} > \dots > a_{q_{len}}\),\(j\) 的取值也就只存在于队列中的点,只需要处理队列中转移方程的最小值即可。
这里可以用树状数组完成,但复杂度为 \(O(n \log n)\)。若是追求 \(O(n)\),可以使用两个单调栈,分别以当前队列的中点为起点向两端维护,当窗口滑动导致单调栈被破坏时暴力重构即可,可以证明这样做复杂度是 \(O(n)\) 的。
严谨证明:
我们发现,当队列元素越多,重构单调栈的次数也就越多,但总的重构次数也越少(仅在对队列弹出过期元素可能重构)。可以考虑把随机数据拆成几段单调下降的序列,最长的最多是最长下降子序列,假设长度为 p。则最多重构 n/p 次,每次最多重构 p 个数,则复杂度最多 O(n/p⋅p)=O(n)。取到最大时,当最长子序列尽量长,也即 h 严格单调下降。当然并不是严格单调下降就可以取到最大值,还要考虑 h 和 m 之间的关系。
(摘自link)
Code:
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 7;
int n;
ll m;
ll h[N], sum[N], tmp[N], f[N];
int q[N], hh = 1, tt;
int s[N][2], top[2], mid;
void push(int i, int op){
if(!top[op] || tmp[s[top[op]][op]] > tmp[i]) s[++top[op]][op] = i;
}
void rebuild(){
mid = (hh + tt) >> 1;
top[0] = top[1] = 0;
for(int i = mid; i >= hh; i--) push(i, 0);
for(int i = mid + 1; i <= tt; i++) push(i, 1);
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> h[i];
sum[i] = sum[i - 1] + h[i];
}
for(int i = 1, j = 1; i <= n; i++){
while(sum[i] - sum[j - 1] > m) j++;
while(hh <= tt && h[i] >= h[q[tt]]){
if(top[0] && s[top[0]][0] == tt) top[0]--;
if(top[1] && s[top[1]][1] == tt) top[1]--;
if(--tt <= mid) rebuild();
}
if(hh > tt) tmp[tt + 1] = f[j - 1] + h[i];
else tmp[tt + 1] = f[q[tt]] + h[i];
q[++tt] = i;
push(tt, 1);
if(top[0] && s[top[0]][0] == hh) top[0]--;
if(top[1] && s[top[1]][1] == hh) top[1]--;
while(hh <= tt && q[hh] < j){
if(++hh > mid) rebuild();
if(top[0] && s[top[0]][0] == hh) top[0]--;
if(top[1] && s[top[1]][1] == hh) top[1]--;
}
f[i] = f[j - 1] + h[q[hh]];
if(top[0]) f[i] = min(f[i], tmp[s[top[0]][0]]);
if(top[1]) f[i] = min(f[i], tmp[s[top[1]][1]]);
}
printf("%lld\n", f[n]);
return 0;
}
浙公网安备 33010602011771号