DP 基础题乱做

恶补基础 DP.

2025.10.22

CF1061C 多样性

转移是经典的子序列 DP,考虑前 \(i\) 个数,子序列长度为 \(j\) 的方案数. 转移:

\[f_{i,j}=\begin{cases} f_{i-1,j-1}+f_{i-1,j}&j\mid a_i\\ f_{i-1,j}&otherwise. \end{cases} \]

可以滚动数组优化一维. 考虑优化,第二种转移直接继承,第一种转移不必枚举 \(j\),可以 \(O(\sqrt v)\) 处理出所有约数,从大到小转移(与 \(0/1\) 背包原理类似). 总时间复杂度 \(O(n\sqrt v)\).

CF2121H 冰宝贝

看到范围是区间,一个直接的想法是离散化. 然而我们可以换维,直接把值域当做 DP 值,把子序列长度开一维状态来记录. 另一个贪心的想法是我们一定要让子序列末尾尽量小,于是设计出一个状态:\(f_{i,j}\) 表示考虑前 \(i\) 个区间,不降子序列长度为 \(j\) 时末尾最小值,转移:

\[f_{i,j}=\begin{cases} f_{i-1,j-1}&l_i\le f_{i-1,j-1}\le r_i\\ f_{i-1,j} &otherwise. \end{cases} \]

优化仍然可以先滚动一维. 从 \(f\) 下标的角度看转移,就是区间 \([l_i,r_i]\) 整体右移了一位. 若当前最长子序列的末尾不大于 \(r_i\),右移后不会有重叠的位置,最长子序列 \(+1\). 否则存在一个位置 \(p\) 重叠,考虑保留哪个更优. 注意到 \(f_j\) 显然不降,于是应该删掉原来 \(p\) 位置上的值,最长子序列长度不变.

考虑维护,实际上我们只关心查询最长子序列的长度,维护在有序的一列数中插入一个数,删除一个数,并且补位. 发现所有操作都可以用 multiset 维护,时间复杂度 \(O(\sum n\log n)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int maxn = 2e5 + 10;
int T, n;

void solve() {
    cin >> n;
    multiset<int> s; s.insert(0);
    while(n--) {
        int l, r; cin >> l >> r;
        auto p = s.upper_bound(r);
        s.insert(l); if(p != s.end()) s.erase(p);
        cout << s.size() - 1 << " ";
    } cout << "\n";
    return;
}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> T; while(T--) solve();

    return 0;
}

AT_agc054_b [AGC054B] 贪心划分

agc 特有的结论计数题,鉴定为纯粹的脑筋急转弯.

结论:确定两人最终选择的物品集合后,两人拿物品的排列唯一确定,即二者构成双射. 可能不难感受到正确性,然而独立想到并不容易.

所以直接背包跑选 \(i\) 个物品凑到 \(sum\over2\) 的方案数,再赋予排列顺序加起来即可. 时间复杂度 \(O(n\sum w_i)\).

AT_agc054_c [AGC054C] 粗略排序

神人题.

考虑对于一个排列怎么交换使其合法. 发现由于所有数都比 \(1\) 大,\(1\) 至少要挪到位置 \(k+1\),除非本来就在 \(k+1\) 前面,同理可以推广到数 \(i\) 至少要挪到 \(k+i\).

那么对于一个操作后的排列,\(p_i=i+k\) 的位置原来就只可能在区间 \([i+k,n]\),乘起来即可. 时间复杂度 \(O(n)\).

2025.10.23

P1858 多人背包

要求背包的前 \(k\) 优解,那么第 \(i\) 优解一定是从第 \(i-1\) 解基础上更新得到的. 考虑朴素 \(0/1\) 背包的转移:

\[f_{i,j}\leftarrow \max_{j=w_i}^V (f_{i-1,j},f_{i-1,j-w_i}+v_i) \]

求第 \(i\) 解时,要么直接继承第 \(i-1\) 解,要么用当前的物品去替换原来的容量更新价值. 所以一共有 \(2k\) 种情况,全部求出来取前 \(k\) 大更新即可.

然而也可以在线地做,观察到两种转移值分别是长度为 \(k\) 的递减序列,拿两个指针扫即可. 时间复杂度 \(O(knV)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 2e2 + 10, maxv = 5e3 + 10, maxk = 55;
int K, V, n, v[maxn], w[maxn];
ll ans, f[maxv][maxk];

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> K >> V >> n; for(int i = 1; i <= n; i++) cin >> w[i] >> v[i];
    memset(f, -0x3f3f, sizeof f);
    f[0][1] = 0;
    for(int i = 1; i <= n; i++) {
        for(int j = V; j >= w[i]; j--) {
            ll tmp[maxk];
            for(int k = 1, p1 = 1, p2 = 1; k <= K; k++) {
                if(f[j][p1] >= f[j - w[i]][p2] + v[i]) tmp[k] = f[j][p1++];
                else tmp[k] = f[j - w[i]][p2++] + v[i];
            } memcpy(f[j], tmp, sizeof f[j]);
        }
    } for(int i = 1; i <= K; i++) ans += f[V][i];
    cout << ans;

    return 0;
}

P4158 [SCOI2009] 粉刷匠

\(f_{i,j}\) 表示花 \(j\) 步填前 \(i\) 行能涂对的最多格子数. 发现这个东西根本转移不了,要想转移,我们还需要知道花若干步填一行最多能填多少个,设 \(g_{i,j,k}\) 表示第 \(i\) 行前 \(j\) 个数,花 \(k\) 步最多能涂多少格子,发现 \(f\) 的转移形如背包,有:

\[f_{i,j}\leftarrow f_{i-1,j-t}+g_{i,m,t} \]

\(g_{i,j,k}\) 的转移比较简单粗暴,枚举从前缀 \(x\) 处转移过来,钦定 \((x,j]\) 全部涂成一个颜色,预处理出一种颜色个数的前缀和,可以直接算出另一种颜色的个数. 所以有转移:

\[g_{i,j,k}\leftarrow \max_{x<j}\Big(g_{i,x,k-1}+\max(sum_j-sum_x,j-x-sum_j+sum_x)\Big) \]

总时间复杂度 \(O(nT+nm^3)\).

P8392 [BalticOI 2022] Uplifting Excursion

神秘题目.

首先有一个贪心是先把所有物品选了,然后从大到小删除,因为小的选了比大的更优. 这时一定存在方案把 \(sum\) 控制在 \([L-m,L+m]\).

记录下每个物品用了多少,考虑用剩下的物品把 \(sum\) 调整到 \(L\). 有结论是在调整过程中出现之前出现过的 \(sum\) 一定不会更优,因为原来求得的 \(sum\) 从最大值开始删,得到的是最优解. 所以为了保证正确性,我们需要按物品从小到大添加. 由于值域限制为 \(O(m)\),所以增减物品的次数是 \(O(m)\) 的,进一步的背包容量就为 \(O(m^2)\).

直接跑多重背包的二进制分组优化,时间复杂度 \(O(m^3\log m)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxm = 3e2 + 10;
int m; ll l, a[maxm << 1], b[maxm << 1];
ll um, dm, sz, f[maxm * maxm << 1];

void add(ll w, ll v, int c) {
    if(w > 0) {
        for(int s = 1; c > 0; c -= s, s <<= 1) {
            int k = min(s, c);
            for(int i = sz; i >= k * w; i--) f[i] = max(f[i], f[i - k * w] + k * v);
        }
    }
    else {
        for(int s = 1; c > 0; c -= s, s <<= 1) {
            int k = min(s, c);
            for(int i = 0; i - k * w <= sz; i++) f[i] = max(f[i], f[i - k * w] + k * v);
        }
    }
}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> m >> l;
    for(int i = 0; i <= 2 * m; i++) {
        cin >> a[i], b[i] = a[i];
        if(i < m) dm += a[i] * (i - m);
        else um += a[i] * (i - m);
    }
    if(l < dm || l > um) cout << "impossible", exit(0);

    ll sum = um + dm, tmp;
    if(sum > l) {
        for(int i = 2 * m; i > m; i--) {
            tmp = sum - l;
            b[i] -= min(tmp / (i - m), a[i]);
            sum -= (i - m) * (a[i] - b[i]);
        }
    }
    else {
        for(int i = 0; i < m; i++) {
            tmp = l - sum;
            b[i] -= min(tmp / (m - i), a[i]);
            sum -= (i - m) * (a[i] - b[i]);
        }
    }

    sz = m * m << 1, tmp = sum - (l - m * m);
    memset(f, -0x3f3f, sizeof f); f[tmp] = 0;
    for(int i = 0; i <= 2 * m; i++) f[tmp] += b[i];
    for(int i = 0; i <= 2 * m; i++) {
        if(i == m) continue;
        if(b[i]) add(m - i, -1, min(b[i], sz));
        if(a[i] - b[i]) add(i - m, 1, min(a[i] - b[i], sz));
    }
    if(f[m * m] < 0) cout << "impossible";//m^2=tmp-sum+l
    else cout << f[m * m];

    return 0;
}

P4170 [CQOI2007] 涂色

一定程度上有贪心的感觉. 最优的涂色方案只会有两种情况:分别涂无交的连续颜色段,或者涂相互包含的颜色段.

所以可以直接据此得到两个转移. 设 \(f_{i,j}\) 表示区间 \([i,j]\) 染色的最小步数,有:

\[f_{i,j}\leftarrow \min _{k=i}^{j-1}(f_{i,k}+f_{k+1,j}) \]

\[f_{i,j}\leftarrow \min_{c_i=c_j}(f_{i+1,j},f_{i,j-1}) \]

所有转移取最小值,从小区间转移到大区间. 时间复杂度 \(O(n^3)\).

回忆起之前 vp 到的另一个 DP,也是确定最优策略之后设计 DP 状态和转移:CF2115C.

P2135 方块消除

很诡异的一个题.

首先正常的区间 DP 无法解决这个问题,因为直接拆成两个区间来消除显然不一定最优. 进一步地,我们希望在消除时钦定一种颜色与区间外的一些同色格子一起在消除完区间内其它格子后再消除.

一个看起来很对的 DP,设 \(f_{i,j,k}\) 表示消除区间 \([i,j]\) 并上后面 \(k\) 个与 \(j\) 同色的的最大代价,\(k\) 个同色段是容易预处理的. 高高兴兴地写出第一个转移:

\[f_{i,j,k}\leftarrow f_{i,j-1,0}+w_{j,k} \]

我们还需要考虑一种可能是区间内有一个与 \(j\) 同色的段 \(k\),先消除 \([k+1,j-1]\),再消除 \([i,k]\) 与后面 \(j\) 与同色段的并. 然而我们的状态无法处理不连续的同色段,所以必须改进.

考虑值域较小,我们可以直接把 \(k\) 改成同色格子数. 充分性显然,考虑必要性,发现把一个完整段拆开不可能增加贡献,所以在转移过程中被优化掉了. 这其中蕴含着错解不优的性质.

所以我们列出完整的转移:

\[f_{i,j,k}\leftarrow f_{i,j-1,0}+(cnt_j+k)^2 \]

\[f_{i,j,k}\leftarrow \max_{l=i}^{j-2}[col_l=col_j](f_{i,l,k+cnt_j}+f_{k+1,j-1,0}) \]

转移取最大值.

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int maxn = 60;
int n, col[maxn], cnt[maxn]; 
int suf[maxn], f[maxn][maxn][maxn * maxn << 1];

inline int pw(const int &x) {return x * x;}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> n; for(int i = 1; i <= n; i++) cin >> col[i];
    for(int i = 1; i <= n; i++) cin >> cnt[i];

    memset(f, -0x3f, sizeof f);
    for(int i = 1; i <= n; i++) for(int j = i + 1; j <= n; j++) if(col[i] == col[j]) suf[i] += cnt[j];
    for(int i = 1; i <= n; i++) for(int j = 0; j <= suf[i]; j++) f[i][i][j] = pw(cnt[i] + j);

    for(int len = 1; len <= n; len++) {
        for(int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            for(int k = 0; k <= suf[j]; k++) f[i][j][k] = max(f[i][j][k], f[i][j - 1][0] + pw(cnt[j] + k));
            for(int k = i; k < j - 1; k++) if(col[k] == col[j]) {
                for(int t = 0; t <= suf[j]; t++) f[i][j][t] = max(f[i][j][t], f[i][k][t + cnt[j]] + f[k + 1][j - 1][0]);
            }
        }
    } cout << f[1][n][0];

    return 0;
}

上述 DP 状态的第三维可以看成是基于值域将颜色段选择的状态状压. 一个可能是,当段数限制在指数级别可以接受的数量级,而方块数极大时,可能要使用状压下标来处理第三维状态. 这时时间复杂度大概是 \(O(m^32^m)\),好处是与值域无关了().

2025.10.24

P3174 [HAOI2009] 毛毛虫

发现答案最终的形态一定是子树内最大和次大毛毛虫并起来,于是考虑 DP 求子树内最大毛毛虫. 毛毛虫可以拆到每个点单独贡献,具体地,除了两端贡献为 \(d_u+1-1\),其余点的贡献为 \(d_u+1-2\).

image

如图,减去原树链上相邻的点算重的贡献.

令所有点的贡献先减掉 \(fa_u\) 处重复的,在此基础上再减去儿子处的并与 \(0\)\(\max\),即可实现贡献的统计. 所以有转移:

\[f_{u}=1+\max(0, d_u-[fa_u\neq -1]-1)+\max_{v\in son_u}f_v \]

考虑在每个点统计子树内最大毛毛虫和次大毛毛虫并的最大值,维护类似于次短路,这里不再赘述. 合并时减去两条毛毛虫各算重的一个点的贡献即可.

image

时间复杂度 \(O(n)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int maxn = 3e5 + 10;
int n, m;
int ans, f[maxn];

vector<int> e[maxn];
void dfs(int u, int fa) {
    int mx1 = 0, mx2 = 0;
    for(int v : e[u]) {
        if(v == fa) continue; 
        dfs(v, u), f[u] = max(f[u], f[v]);
        if(f[v] > mx1) mx2 = mx1, mx1 = f[v];
        else if(f[v] > mx2) mx2 = f[v];
    }
    f[u] += 1 + max(0, (int)e[u].size() - (fa != -1) - 1);
    ans = max(ans, mx1 + mx2 + 1 + max(0, (int)e[u].size() - 2));
}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> n >> m; 
    for(int i = 1, u, v; i <= m; i++) cin >> u >> v, e[u].push_back(v), e[v].push_back(u);
    dfs(1, -1); cout << ans;

    return 0;
}

P3177 [HAOI2015] 树上染色

要直接统计所有同色点对之间的距离之和是困难的,但是经过一条边的点对数量是容易计算的,所以可以把贡献拆到每条边计算. 不妨认为初始时整个树同色,DP 时对一些点染色. 设 \(f_{u,i}\) 表示子树 \(u\) 内染 \(i\) 个点的最大贡献. 有转移:

\[f_{u,i}\leftarrow f_{u,i-j}+f_{v,j}+w_{u,v}\times \Big(j\times(k-j)+(sz_v-j)\times(n-k-sz_u-j)\Big) \]

转移形如 \(0/1\) 背包,那么 \(i\) 当然应该从大到小枚举. \(j\) 的枚举涉及到一些边界问题,当 \(j=0\) 时要用原来的 \(f_{u,i}\) 更新自己,但是如果从大到小枚举,之前已经更新过 \(f_{u,i}\)\(j=0\) 的更新就出现了错误. 解决方法可以在最开始先转移 \(j=0\),但是最好另开一个临时数组来存,最后统一更新答案. 各种边界问题在短时间搞明白并不容易. 在排除不合法的转移之后,时间复杂度 \(O(n)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 2e3 + 10;
int n, k;

vector<array<int, 2> > e[maxn];

int sz[maxn]; ll f[maxn][maxn];
void dfs(int u, int fa) {
    sz[u] = 1;
    for(auto [v, w] : e[u]) {
        if(v == fa) continue;
        dfs(v, u), sz[u] += sz[v];
        for(int j = min(sz[u], k); j >= 0; j--) {
            for(int l = max(0, j - (sz[u] - sz[v])); l <= min(sz[v], j); l++) {//之前染过颜色的点个数上界是 sz[u]-sz[v],可以推下界
                ll tmp = 1ll * l * (k - l) + 1ll * (sz[v] - l) * (n - k - (sz[v] - l));
                f[u][j] = max(f[u][j], f[u][j - l] + f[v][l] + tmp * w);
            }
        }
    }
}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

    cin >> n >> k; k = min(k, n - k);
    for(int i = 1, u, v, w; i < n; i++) cin >> u >> v >> w, e[u].push_back({v, w}), e[v].push_back({u, w});
    dfs(1, -1); cout << f[1][k];

    return 0;
}

P3523 [POI 2011] DYN-Dynamite

读错题了qwq.

首先发现这个限制具有单调性,可以直接转换成二分判断可行性. 如果没有 \(d_i\) 的限制是容易的,直接从叶子开始向上贪心地选即可:具体的,距离叶子为 \(mid\) 的先选择,此后每距离 \(2\times mid\) 选择一个点.

加上 \(d_i\) 的限制后考虑 DP. 根据上面的贪心,我们需要在状态中记录 \(u\) 到子树中最远的未被覆盖\(d_i=1\) 的点 \(i\) 的距离,以及子树内距离最近的被选择 \(d_i=1\) 的点. 不妨设前者为 \(f_u\),后者为 \(g_u\).

有基本的转移:

\[f_u=\max_{v\in son_u}(f_u,f_v+1) \]

\[g_u=\min_{v\in son_u}(g_u,g_v+1) \]

根据题意,还有:

  • \(f_u+g_u\le mid\) 时,子树 \(u\) 被完全覆盖,令 \(f_u=-\infty\).
  • \(d_u=1\wedge g_u>mid\)\(u\) 是未被覆盖且可以选择的点,有转移 \(f_u\leftarrow\max(f_u,0)\).
  • \(f_u=mid\),选择最远未被覆盖的 \(d_i=1\) 的点 \(i\) 一定不劣,答案 \(+1\),更新 \(f_u=-\infty,g_u=0\).

最后还需要特判根节点是否被覆盖,否则答案额外 \(+1\). 时间复杂度 \(O(n)\).

点击查看代码
#include<bits/stdc++.h>
using namespace std;

const int maxn = 3e5 + 10, inff = 1e9;
int n, m; bool d[maxn];
int ans, cnt, f[maxn], g[maxn];

int mid;

vector<int> e[maxn];
void dfs(int u, int fa) {
    f[u] = -inff, g[u] = inff;
    for(int v : e[u]) {
        if(v == fa) continue;
        dfs(v, u);
        f[u] = max(f[u], f[v] + 1);
        g[u] = min(g[u], g[v] + 1);
    }
    if(f[u] + g[u] <= mid) f[u] = -inff;
    if(d[u] && g[u] > mid) f[u] = max(f[u], 0);
    if(f[u] == mid) f[u] = -inff, g[u] = 0, cnt++;
}

inline bool check() {
    cnt = 0, dfs(1, 0);
    if(f[1] >= 0) ++cnt;
    return cnt <= m;
}

int main() {
    ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
    
    cin >> n >> m; for(int i = 1; i <= n; i++) cin >> d[i];
    for(int i = 1, u, v; i < n; i++) cin >> u >> v, e[u].push_back(v), e[v].push_back(u);
    
    int l = 0, r = n;
    while(l <= r) {
        mid = l + r >> 1;
        if(check()) ans = mid, r = mid - 1;
        else l = mid + 1;    
    } cout << ans;

    return 0;
}
posted @ 2025-10-22 21:45  Ydoc770  阅读(10)  评论(1)    收藏  举报