DP 做题记录

\(2022.9.19 NOIP\) 模拟赛】能量骰子

\(Problem:\)
你有 \(A\) 枚正能量骰子,\(6\) 个面分别显示的是 \([+1, +6]\) 的值。
你有 \(B\) 枚负能量骰子,\(6\) 个面分别显示的是 \([-6, -1]\) 的值。
你同时投掷这 \(A+B\) 枚骰子,如果存在一枚正能量骰子和一枚负能量骰子点值的和为 \(0\),它们会结对消失。
问结对消失过程结束后,存在点数不超过 \(T\) 的正能量骰子的概率,在模 \(998244353\) 意义下的值。
\(A,B <= 250\)

\(Solution:\)
image

const int N = 250 + 10, mod = 1e9 + 7   ;
int T, a, b, ans; int f[10][N][N], s[N][N][N], fac[N], inv[N];
inline int qpow(int a, int b){ int res = 1; while(b){ if(b & 1){ res = res * a % mod; } a = a * a % mod, b >>= 1; } return res; }
inline int C(int n, int m){ if(m > n){ return 0; } return fac[n] * inv[m] % mod * inv[n - m] % mod; }

signed main(){
    read(T), read(a), read(b); fac[0] = fac[1] = 1; inv[0] = 1; f[0][0][0] = 1;
    for(int i = 1; i <= 250; i ++) fac[i] = fac[i - 1] * i % mod; inv[250] = qpow(fac[250], mod - 2);
    for(int i = 249; i; i --) inv[i] = inv[i + 1] * (i + 1) % mod;
    for(int i = 0; i <= b; i ++) for(int j = 0; j <= a; j ++) for(int p = j; ~ p; p --)
        s[i][j][p] = (s[i][j][p + 1] + C(j, p) * f[0][i][p] % mod) % mod;
    for(int i = 1; i <= T; i ++){
        for(int j = 0; j <= b; j ++) for(int k = 0; k <= a && k <= j; k ++) for(int p = 0; p <= j; p ++)
            f[i][j][k] = (f[i][j][k] + C(j, p) * s[j - p][k][max(k - p, 0ll)] % mod) % mod;
        for(int j = 0; j <= b; j ++) for(int p = 0; p <= a; p ++){
            s[j][p][p + 1] = 0;
            for(int k = p; ~ k; k --) s[j][p][k] = (s[j][p][k + 1] + C(p, k) * f[i][j][k] % mod) % mod;
        }
    }
    for(int i = 0; i <= b; i ++) for(int j = 0; j <= a; j ++) 
        ans = (ans + f[T][i][j] * qpow(6 - T, b - i) % mod * qpow(6 - T, a - j) % mod * C(b, i) % mod * C(a, j) % mod) % mod;
    ans = ans * qpow(qpow(6, a + b), mod - 2) % mod;
    ans = (1 - ans + mod) % mod; return print(ans), 0;              
}

\(CF1691F K-Set Tree\)

\(Problem:\) 给定一棵有 \(n\) 个点的树,定义 \(f(r,S)\) 为以 \(r\) 为根时包含集合 \(S\) 中所有点的最小子树的大小。给定常数 \(k\) ,对于每一对满足 \(|S|=k\)\(S\)\(r\),求所有 \(f(r,S)\) 之和,即求 \(\sum_{r=1}^{n}\sum_{|S|=k}f(r,S)\)

\(Solution:\) 固定根,枚举 LCA 即可,\(\sum s z_{i}\left(\left(\begin{array}{c} s z_{i} \\ k \end{array}\right)-\sum_{j \in s o n(i)}\left(\begin{array}{c} s z_{j} \\ k \end{array}\right)\right)\)
一个点的贡献是 \(\left(s z_{i}-s z_{f a_{i}}\right)\left(\begin{array}{c}s z_{i} \\ k\end{array}\right)\)
换根 \(dp\)\(dfs\) 实现。具体实现将 \(fa_{i}\) 分三类枚举:

  • 没有 \(fa_{i}\),就是 \(n\left(\begin{array}{l}n \\ k\end{array}\right)\)
  • \(fa_{i}\)\(i\) 子树里,记录子树里每个根对应的 \(sz_{j}\) 总和。
  • \(fa_{i}\) 就是以 \(1\) 为根的树上 \(i\) 的父亲,记录每个根对应的 \(sz_{fa_{i}}\) 的总和。
const int N = 2e5 + 10, mod = 1e9 + 7;
int n, k; int h[N], e[N << 1], ne[N << 1], idx, ans;
inline void add(int a, int b){ e[idx] = b, ne[idx] = h[a], h[a] = idx ++; }
inline int qpow(int a, int b){ int res = 1; while(b){ if(b & 1) res = res * a % mod; a = a * a % mod, b >>= 1; } return res; }
int fac[N], inv[N], size[N], sonsize[N];
inline int C(int n){ return n < k ? 0 : fac[n] * inv[k] % mod * inv[n - k] % mod; }
inline void dfs(int u, int fa = 0){
    size[u] = 1;
    for(int i = h[u]; ~ i; i = ne[i]){
        int v = e[i]; if(v == fa) continue;
        dfs(v, u); size[u] += size[v];
    }
}
inline void calc(int u, int fa = 0){
    int fasize = (n + (n - size[u]) * size[u]) % mod;
    sonsize[u] = n; for(int i = h[u]; ~ i; i = ne[i]){
        int v = e[i]; if(v == fa) continue; 
        fasize = (fasize + size[v] * (n - size[v])) % mod;
    } for(int i = h[u]; ~ i; i = ne[i]){
        int v = e[i]; if(v == fa) continue;
        ans = (ans + (mod + size[v] * (n - size[v]) % mod - (fasize + mod - size[v] * (n - size[v]) % mod) % mod) * C(size[v])) % mod;
        calc(v, u); sonsize[u] = (sonsize[u] + size[v] * (n - size[v])) % mod;
        ans = (ans + (mod + (n - size[v]) * size[v] % mod - sonsize[v]) * C(n - size[v])) % mod;
    } return;
}
signed main(){
    memset(h, -1, sizeof h); read(n), read(k); fac[0] = inv[0] = 1; 
    for(int i = 1; i <= n; i ++) fac[i] = fac[i - 1] * i % mod; inv[n] = qpow(fac[n], mod - 2);
    for(int i = n - 1; i >= 1; i --) inv[i] = inv[i + 1] * (i + 1) % mod;
    for(int i = 1, u, v; i < n; i ++){ read(u), read(v); add(u, v), add(v, u); }
    ans = n * n % mod * C(n) % mod; dfs(1), calc(1); return print(ans), 0;
}

\(CF337D Book of Evil\)

\(Problem:\) 给定一颗 \(n\) 个节点的数,有 \(m\) 个节点有怪物。树上有一本魔法书,魔法书可以使距离小于等于 \(d\) 的点出现怪物,求魔法书所在点有几种可能。(所有的怪物都是由魔法书引发的)

\(Solution:\)\(f[i][1]\) 表示 \(i\) 到其子树内有怪物的点的最大距离,\(f[i][2]\) 表示 \(i\) 到其子树内有怪物的点的次大距离,\(dis[i]\) 表示 \(i\) 到其子树以外的点的最大距离。最后 $f[i][1] <= d $ 并且 \(dis[i] <= d\)\(ans\) 加一。

const int N = 1e5 + 10;
int n, m, d, a[N]; int h[N], e[N << 1], ne[N << 1], idx; int f[N][3], dis[N];
inline void add(int a, int b){ e[idx] = b, ne[idx] = h[a], h[a] = idx ++; }
inline void dfs1(int u, int fa){
    if(a[u]) f[u][1] = f[u][2] = 0;
    for(int i = h[u]; ~ i; i = ne[i]){ int j = e[i]; if(j == fa) continue; dfs1(j, u); 
        if(f[j][1] + 1 > f[u][1]) f[u][2] = f[u][1], f[u][1] = f[j][1] + 1; 
        else f[u][2] = max(f[u][2], f[j][1] + 1);
    }
}
inline void dfs2(int u, int fa){
    for(int i = h[u]; ~ i; i = ne[i]){
        int j = e[i]; if(j == fa) continue;
        if(f[j][1] + 1 == f[u][1]) dis[j] = max(dis[u] + 1, f[u][2] + 1);
        else{ dis[j] = max(dis[u] + 1, f[u][1] + 1); } dfs2(j, u);
    }
}
signed main(){
    memset(dis, 128, sizeof dis); memset(f, 128, sizeof f); memset(h, -1, sizeof h); read(n), read(m), read(d);
    for(int i = 1; i <= m; i ++){ int p; read(p); a[p] = 1; }
    for(int i = 1, u, v; i <= n - 1; i ++){ read(u), read(v); add(u, v), add(v, u); }
    dfs1(1, 0); dfs2(1, 0); int ans = 0;
    for(int i = 1; i <= n; i ++){
        if(i == 1){ if(f[i][1] <= d && f[i][2] <= d) ans ++; }
        else if(dis[i] <= d && f[i][1] <= d) ans ++;
    } return print(ans), 0;
}

\(CF771C Bear and Tree Jumps\)

\(Problem:\) 每次可以从 \(u\) 跳到距离小于等于 \(k\) 的任意节点,求所有点对间跳到达的步数和。
\(Solution:\) 考虑点 \(u\) 到它的子树中节点的答案。记 \(f_{u,i}\) 表示与 \(u\) 子树中与它距离为 \(i\) 的节点的答案和。转移时,对于 \(i\neq 0\) 可以直接对儿子的答案求和;而 \(i=0\) 时,分两部分计算:

  1. \(u\) 距离小于 \(k\) 的节点,它们可以从 \(u\) 一步跳到,每个节点的贡献为 \(1\)
  2. \(u\) 距离不小于 \(k\) 的节点,可以从 \(u\) 往下先跳 \(k\) 的距离,再由子树中的 \(DP\) 值转移。具体地,可以从 \(u\) 的儿子中 \(i=k-1\) 的状态转移,此外,每个节点的贡献还需要加上刚开始跳的一下。

然后发现 \(u\) 子树中除了 \(u\) 自己以外每个节点都有至少 \(1\) 的贡献,把它单独拿出来就是 \(size_u-1\);剩下的可以每个儿子 \(O(k)\)求和转移。

\[\begin{array}{c} f_{u, 0}=\sum_{v \in s o n_{u}} f_{v, k-1}+s i z e_{v} \\ f_{u, i}=\sum_{v \in \operatorname{son} n_{u}} f_{v, i-1} \end{array}\]

然后从上到下再次 DFS 一遍,计算父亲方向的连通分量对 \(u\) 的贡献。转移同理,最后题目要求所有无序对的答案,加起来以后除个 \(2\)

const int N = 2e5 + 10;
int n, k; int h[N], e[N << 1], ne[N << 1], idx; int f[N][5], size[N];
inline void add(int a, int b){ e[idx] = b, ne[idx] = h[a], h[a] = idx ++; }
inline void dfs1(int u, int fa){
    size[u] = 1; for(int i = h[u]; ~ i; i = ne[i]){
        int j = e[i]; if(j == fa) continue;
        dfs1(j, u); size[u] += size[j]; f[u][0] += f[j][k - 1] + size[j];
        for(int i = 1; i < k; i ++) f[u][i] += f[j][i - 1];
    }
}
inline void dfs2(int u, int fa){
    int t[5] = {0};
    if(fa){ t[0] = f[fa][0] - f[u][k - 1] - size[u]; for(int i = 1; i < k; i ++) t[i] = f[fa][i] - f[u][i - 1]; }
    f[u][0] += t[k - 1] + n - size[u];
    for(int i = 1; i < k; i ++) f[u][i] += t[i - 1];
    for(int i = h[u]; ~ i; i = ne[i]){ if(e[i] != fa) dfs2(e[i], u); }
}

signed main(){
    read(n), read(k); memset(h, -1, sizeof h);
    for(int i = 1, u, v; i <= n - 1; i ++){ read(u), read(v); add(u, v), add(v, u); }
    dfs1(1, 0); dfs2(1, 0); int ans = 0;
    for(int i = 1; i <= n; i ++) ans += f[i][0];
    return print(ans >> 1), 0;
}

\(CF963B Destruction of a Tree\)

\(Problem:\) 给定一棵树,节点度数为偶数可删除该节点,并删除相连的边,给出删除整棵树的方案。
\(Solution:\) 显然叶子结点不能第一个删除,设 \(f[u]\) 表示先删自己 \((0)\) 还是先删除父亲 \((1)\)。显然所有叶子节点的 \(f\) 值为 \(1\)。对于任意一个节点有方程:\(f[u]=\text { degree }_{u} \bigoplus_{v \in \text { son }_{u}}\{f[v]\}\)\(degree\) 指奇偶性。\(f[root] = 1\) 为无解。

const int N = 2e5 + 10;
int h[N], tot, n, f[N], e[N << 1], ne[N << 1], idx;
inline void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
inline void dfs(int u, int fa){
    for(int i = h[u]; ~ i; i = ne[i]) if(e[i] != fa) dfs(e[i], u), f[u] ^= f[e[i]];
    f[u] ^= 1;
}
inline void calc(int u,int fa){
    for(int i = h[u]; ~ i; i = ne[i]) if(e[i] != fa && f[e[i]]) calc(e[i], u);
    print(u), puts("");
    for(int i = h[u]; ~ i; i = ne[i]) if(e[i] != fa && !f[e[i]]) calc(e[i], u);
}
signed main(){
    memset(h, -1, sizeof h); read(n);
	for(int i = 1; i <= n; i ++){
		int x; read(x); if(x) add(x, i), add(i, x), f[i] ^= 1, f[x] ^= 1;
	}
	dfs(1, 0); if(!f[1]) return puts("NO"), 0;
	puts("YES"); calc(1,0); return 0;
}

\(CF1151E Number of Components\)

\(Problem:\)\(\sum_{i=1}^{n} \sum_{j=i}^{n} f(i, j)\) 其中 \(f(i,j)\) 表示只保留权值在 \([i,j]\) 之间的点形成的连通块数量。
\(Solution:\) \(Hs\_black's blog\)

const int N = 1e5 + 10;
int n, ans, a[N];

signed main(){
    read(n); for(int i = 1; i <= n; i ++) read(a[i]);
    for(int i = 1; i <= n; i ++){
        if(a[i + 1] > a[i]) ans += (a[i + 1] - a[i]) * a[i];
        else ans += (n - a[i] + 1) * (a[i] - a[i + 1]);
    } print(ans); return 0;
}

\(CF722E Research Rover\)

\(Problem:\)\((1,1)\) 走到 \((n,m)\) 的道路中有 \(k\) 个特殊点,每经过一个特殊点会使分数变为原来一半,问从 \((1,1)\)\((n,m)\) 的期望得分。

\(Solution:\) \(f_{i,j}\) 表示表示到 \(i\) 点至少经过 \(j\) 个特殊点的方案数,对于 \(f_{i,j}\) 的转移我们考虑从 \(j-1\) 转移过来。\(f_{i, j}=\sum f_{k, j-1}-f_{k, j}\left(x_{k} \leq x_{i}, y_{k} \leq y_{i}\right)\) 其中 \((f_{k,j-1}-f_{k,j})\) 表示经过 \(j-1\) 个特殊点到达 \(i\) 的方案数。则 \(ans = \left(C_{n+m}^{n}\right)^{-1} \times \sum\left(f_{k, i}-f_{k, i+1}\right) \times\left(s / 2^{i}\right)\)

const int mod = 1e9 + 7, N = 2e5 + 10;
int n, m, k, s, ans; int inv[N], fac[N], f[N >> 1][31];
struct node{ int x, y; } a[N];
inline bool cmp(node a, node b){ return a.x == b.x ? a.y < b.y : a.x < b.x; }
inline int qpow(int a, int b){ int res = 1; while(b){ if(b & 1) res = res * a % mod; a = a * a % mod, b >>= 1; } return res; }
inline int C(int x, int y){ return fac[x] % mod * inv[x - y] % mod * inv[y] % mod; }
inline int calc(node a, node b){ return C(b.x - a.x + b.y - a.y, b.x - a.x); }

signed main(){
    read(n), read(m), read(k), read(s); fac[0] = inv[0] = 1;
    for(int i = 1; i <= n + m; i ++){ fac[i] = fac[i - 1] * i % mod; } inv[n + m] = qpow(fac[n + m], mod - 2);
    for(int i = n + m - 1; i >= 0; i --){ inv[i] = inv[i + 1] * (i + 1) % mod; }
    for(int i = 1, x, y; i <= k; i ++) read(x), read(y), a[i] = {x, y};
    sort(a + 1, a + k + 1, cmp); if(a[1].x != 1 || a[1].y != 1){ a[++ k] = {1, 1}; s <<= 1; }
    if(a[k].x != n || a[k].y != m) a[++ k] = {n, m}; else s = (s + 1) / 2; int gs = log2(s) + 1;
    sort(a + 1, a + k + 1, cmp); f[1][0] = 1; 
    for(int i = 2; i <= k; i ++){
        f[i][1] = calc(a[1], a[i]);
        for(int j = 2; j <= gs; j ++) for(int t = 1; t < i; t ++)
            if(a[t].x <= a[i].x && a[t].y <= a[i].y){
                f[i][j] += (f[t][j - 1] * calc(a[t], a[i])) % mod; f[i][j] = (f[i][j] + mod) % mod;
                f[i][j] -= (f[t][j] * calc(a[t], a[i])) % mod; f[i][j] = (f[i][j] + mod) % mod;
            }
    } int ans = 0;
    for(int i = 1; i <= gs; i ++){
        s -= s / 2; ans = (ans + (f[k][i] - f[k][i + 1]) * s % mod + mod) % mod;
    }
    ans = (ans * qpow(calc(a[1], a[k]), mod - 2) + mod) % mod;
    print(ans); return 0;
}

\(P3059 [USACO12NOV]Concurrently Balanced Strings G\)

\(Problem:\) 给出k个长度为 \(n\) 的括号序列,问有多少个区间在 \(k\) 个序列中对应的子串均平衡。

\(Solution:\) \(f[i]\) 表示以 \(i\) 为左端点的合法区间个数。令 \(pos[i]\) 表示以 \(i\) 为左端点,最靠左的合法右端点。 那么有如下转移: \(f[i]=f[pos[i]+1]+1\)

令左括号为 \(1\),右括号为 \(-1\)。合法的区间满足 \(sum[r]−sum[l−1]=0\)\(sum[i]−sum[l−1]>=0\)

分开处理这 \(2\) 个限制。 对于每个左端点 \(i\),右端点不超过使区间前缀为负的第一个点,满足第二点限制。对于同一列的每一行都求出这个值,然后取 \({\min}\),表示第 \(i\) 列的右端点不超 \({\min}\) 就可以对 \(k\) 行都满足第二点限制。\({\min}\) 记为 \(lim[i]\)

枚举列 \(i\),寻找最近的右端点满足当前列的 \(k\) 行与第 \(i−1\) 列的 \(k\) 行相同即可,即 \(pos[i]\)

const int inf = 1e18, N = 1e5 + 10;
int ans, f[N], lim[N], minn[N], n, m;
char s[N]; map<signed int, int> M;

struct node{
    int s[12]; signed int op;
    friend bool operator==(node a, node b){
        for(int i = 1; i <= m; i ++) if(a.s[i] != b.s[i]) return false;
        return true;
    }
}sum[N];

inline void upmax(int &a, int b){ if(b > a) a = b; }
inline void upmin(int &a, int b){ if(b < a) a = b; }

signed main(){
    read(m), read(n);
    for(int i = 1; i <= m; i ++){
        scanf("%s", s + 1); for(int j = 1; j <= n; j ++)
            sum[j].s[i] = sum[j - 1].s[i] + ((s[j] == ')') ? -1 : 1);
    } for(int i = 1; i <= n; i ++) lim[i] = inf;
    for(int i = 1; i <= m; i ++){
        memset(minn, 127, sizeof minn);
        for(int j = n; j; j --){
            upmin(minn[(sum[j].s[i] + 50000)], j);
            upmin(lim[j], minn[(sum[j - 1].s[i] - 1 + 50000)]);
        }
    }
    for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) sum[i].op = sum[i].op * 13 + sum[i].s[j];
    for(int i = n; i; i --){
        int pos = M[sum[i - 1].op];
        if(pos && pos < lim[i]) f[i] = f[pos + 1] + 1, ans += f[i];
        if(M[sum[i].op]) upmin(M[sum[i].op], i); else M[sum[i].op] = i;
    }
    print(ans); return 0;
}

\(AT2000 [AGC002F] Leftmost Ball\)

\(Problem:\) \(n\) 种颜色的球,每个球有 \(k\) 个,把这 \(n\times k\) 个球排成一排,把每一种颜色的最左边出现的球涂成白色,求有多少种不同的颜色序列。

\(Solution:\) 将 $n \times k $ 个球视为 \(n\) 个白球和 \(n \times k - n\) 个其他颜色的球,由于白球是每种颜色的第一个,所以要保证每个位置的前缀白求个数均大于等于其他颜色种类数。设 \(f_{i,j}\) 表示已经放了 \(i\) 个白球和 \(j\) 个其他球的方案数\((i ≥ j)\)

\(Transit:\) 考虑最后一个放的是什么,白球:\(f_{i-1,j}\),新的颜色 \(f_{i,j-1} \times (n-j+1) \times \binom{n×k−i−(j−1)×(k−1)−1}{k−2}\)

const int N = 2e3 + 10, M = 4e6 + 10, mod = 1e9 + 7;
int n, k, f[N][N], fac[M], inv[M];

inline int qpow(int a, int b){ int res = 1; while(b){ if(b & 1) res = res * a % mod; a = a * a % mod, b >>= 1; } return res; }
inline int C(int n, int m){ return (((fac[m] * inv[n]) % mod) * inv[m - n]) % mod; }

signed main(){
    read(n), read(k); if(k == 1) return puts("1"), 0;
    fac[0] = 1; f[0][0] = 1; for(int i = 1; i <= M; i ++) fac[i] = (fac[i - 1] * i) % mod;
    inv[M] = qpow(fac[M], mod - 2); for(int i = M - 1; i >= 0; i --) inv[i] = (inv[i + 1] * (i + 1)) % mod;
    for(int i = 1; i <= n; i ++) for(int j = 0; j <= i; j ++){
        f[i][j] = f[i - 1][j]; if(!j) continue;
        (f[i][j] += f[i][j - 1] * (n - j + 1) % mod * C(k - 2, n - i + (n - j + 1) *(k - 1) - 1) % mod) %= mod;
    } print(f[n][n]); return 0;
}

\(AT2567 [ARC074C] RGB Sequence\)

\(Problem:\) 有一个序列 \(a\),要给序列中的每个元素一种颜色:红/绿/蓝。有 \(m\) 条限制 \((l,r,x)\),表示格子 \(l-r\) 中颜色的种数要恰好为 \(x\),问可行的方案数。

\(Solution:\) 设状态 \(f_{i,j,k}\) 表示当前位置为 \(i\),之前第一个与 \(a_i\) 不同的位置为 \(j\),再之前第一个与 \(a_i,a_j\) 都不同的位置为 \(k\)
对于限制 \((l,r,x)\),若 \((i,j,k)(r =i)\) 满足以下的限制 \(f_{i,j,k}\) 应当为 \(0\)

  1. \(x=1,l<=j\)
  2. \(x=2,l<=k\)\(j<l\)
  3. \(x=3,k<l\)

\(Transit:\)转移到 \(i\) 时对所有 \(r=i\) 的限制剔除不合法转移,之后考虑如何转移。

  1. \(a_{i+1}= a_i\)\(f_{i+1,j,k}\) 继承 \(f_{i,j,k}\)
  2. \(a_{i+1}= a_j\)\(f_{i+1,i,k}\) 继承 \(f_{i,j,k}\)
  3. \(a_{i+1}= a_k\)\(f_{i+1,i,j}\) 继承 \(f_{i,j,k}\)

\[\text { ans }=\sum_{j=0}^{j<n} \sum_{k=0}^{k<j(\text { if } j=0 \text { so } k<1)} f_{n, j, k} \]

const int mod = 1e9 + 7, N = 3e2 + 10;
struct node{
    int l, r, cnt;
    inline int operator <(const node &p) const{ return r < p.r; }
}a[N]; int f[N][N][N], n, m;

signed main(){
    read(n), read(m); for(int i = 1; i <= m; i ++) read(a[i].l), read(a[i].r), read(a[i].cnt);
    sort(a + 1, a + m + 1); f[1][0][0] = 3; int pt = 1;
    for(int i = 1; i <= n; i ++){
        while(pt <= m && a[pt].r <= i){
            for(int j = 0; j < i; j ++){
                int limit = j ? j - 1 : 0;
                for(int k = 0; k <= limit; k ++){
                    if(a[pt].cnt == 1 && a[pt].l <= j) f[i][j][k] = 0;
                    if(a[pt].cnt == 2 && (a[pt].l <= k || j < a[pt].l)) f[i][j][k] = 0;
                    if(a[pt].cnt == 3 && k < a[pt].l) f[i][j][k] = 0;
                }
            } pt ++;
        }
        if(i == n) break;
        for(int j = 0; j < i; j ++){
            int limit = j ? j - 1 : 0;
            for(int k = 0; k <= limit; k ++){
                if(!f[i][j][k]) continue;
                f[i + 1][j][k] = (f[i + 1][j][k] + f[i][j][k]) % mod;
                f[i + 1][i][k] = (f[i + 1][i][k] + f[i][j][k]) % mod;
                f[i + 1][i][j] = (f[i + 1][i][j] + f[i][j][k]) % mod;
            }
        }
    } int ans = 0;
    for(int j = 0; j < n; j ++){
        int limit = j ? j - 1 : 0;
        for(int k = 0; k <= limit; k ++) ans = (ans + f[n][j][k]) % mod;
    }
    print(ans); return 0;
}

\(CF1453F Even Harder\)

\(Problem:\) 给定数组 \(a\)\(a_i\) 表示从 \(i\) 能走到 \([i+1, i+a_i]\),问至少需要把几个 \(a_i\) 改成 \(0\),才能使得 \(1\)\(n\) 有且仅有一条路径。

\(Solution:\) 首先令 \(a_i = a_i + 1\)

发现对于唯一的被走的序列 \(P\),有以下性质:

  1. 对于每个 \(p_i\),一定没有变为 \(0\),且 \(p_i+a_{p_i}>p_{i+1}\),否则不能到达下一个点。
  2. 对于每个 \(p_i\),一定满足 \(p_i + a_{p_i}≤p_{i+2}\),否则将多出一种方案:从 \(p_i\)\(p_{i+2}\)
  3. 对于每个 \(p_i,p_{i+1}\),当 \(p_i<j<p_{i+1}\) 时,一定满足 \(j+a_j≤p_{i+1}\),否则将多出一种方案:从 \(p_i\)\(j\)\(p_{i+1}\)

\(Transit:\)\(f[v][j]\) 表示当前最后一个 \(p_i=v\),然后 \(p_{i-1}\) 可以跳到 \(j\) 之前的最小变 \(0\) 数量。转移过程就是枚举 \(v'\)\(v\),然后从 \(f[v][v']\) 转移到 \(f[v'][v+a_v]\),代价是把 \(v\)\(v'\) 之间可以跳到 \(v'\) 右边的点全部变为 \(0\)
然后把第二维前缀最小值一下。

const int N = 4e3 + 10;
int n, a[N], f[N][N]; 

signed main(){
    int T; read(T); while(T --){
	read(n);
	for(int i = 1; i <= n; i ++) read(a[i]), a[i] += i;
	for(int i = 2; i <= n; i ++){
        int c = 0;
		for(int j = i; j <= n; j ++) f[i][j] = 1e9;
		for(int j = i - 1; j >= 1; j --){
		    if(a[j] < i)continue;
			f[i][a[j]] = min(f[i][a[j]], f[j][i - 1] + c); c ++;
		}for(int j = i + 1; j <= n; j ++) f[i][j] = min(f[i][j], f[i][j - 1]);
	}print(f[n][n]), puts("");
    }
} 
posted @ 2022-08-14 21:58  Altwilio  阅读(43)  评论(0编辑  收藏  举报