DP 选做(长期更新)

在做这个题单:https://www.luogu.com.cn/training/629645
题目按照猎奇程度排序.

CF1016F Road Projects

Hint:抽出 \(1\sim n\) 的链 \(L\) 之后链上每个节点有一棵子树,考虑根据子树的状态分讨,对子树进行处理.

要使最短路最长,但是原先的最短路无法变动. 所以最短路在加一条边发生改变当且仅当出现了一条新的最短路径,并且这条最短路是所有可能新路径中最长的一条.

  • 不必构造新的最短路,只需要存在一个子树大小大于 \(2\),连接子树中未直接相连的两点即可.

  • 需要构造新的最短路,最短路一定是由两个子树到根距离最长的点相连,且每次询问都连接的是相同的两个点. 设子树 \(u\) 中到 \(u\) 最长距离为 \(f_u\),链上每个点到起点的距离为 \(dis_u\),这些可以简单预处理. 有转移:

\[ans\leftarrow \max_{u\neq v\ u,v\in L}\Big(dis_u+f_u+f_v+dis_n-(dis_v)\Big) \]

拿个变量存一下前缀最大的 \(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\) 相同时可以直接转移:

\[f_{i,j}=\max_{b_k=i}(f_{i,j},f_{i,j-a_k}) \]

考虑不同的 \(i\) 怎么合并. 如果给 \(i-1\)\(k\times2^{i}\) 的重量,也就是 \((2k)\times2^{i-1}\),相当于 \(j\) 这一维有 \(2k\) 的可分配重量. 同时由于总价值 \(W\) 拆成二进制也可能有 \(2^{i-1}\) 的贡献,如果有也一并加上. 就有转移:

\[f_{i,j}=\max_{k=0}^j(f_{i-1,2k+((W>>(i-1))\&1)}+f_{i,j-k}) \]

点击查看代码
#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\) 的方案数,有转移:

\[f_{i,j}=f_{i-1,j-1}+f_{i,2\times 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\) 的方案数,有转移:

\[f_{i,j}=f_{i-1,j-b_i}+\sum_xf_{i-1,x} \]

由于 \(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\) 的路径条数,贪心地填数.

\[\begin{aligned} ans&=\sum_{1\le u< v\le n}\text{mex}(u, v)\\ &=\sum_{k=1}^n\left(\sum_{\text{mex}(u,v)=k}k\right)\\ &=\sum_{k=1}^n\left(\sum_{\text{mex}(u,v)\ge k}1\right)\\ \end{aligned} \]

于是考虑贪心地填数使得路径数最大. 容易发现只有所有包含 \(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)\) 的最大路径数,不难得到转移:

\[f_{u,v} = sz_{u,v}\times size_{v, u} + \max\left(f_{u,fa_{u,v}},f_{fa_{v,u},v}\right) \]

记忆化搜索即可通过,时间复杂度 \(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\choose m}\equiv{{\lfloor{n\over k}\rfloor}\choose \lfloor{m\over k}\rfloor}{n\bmod k\choose m\bmod k}\pmod k \]

递归展开右式,得到的式子就是 \(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;
}
posted @ 2025-09-07 18:02  Ydoc770  阅读(14)  评论(0)    收藏  举报