做题记录 (2024-11-21 to 2024-11-29)
CF1922F Replace on Segment *2500,区间DP
可证最优解可以不存在相交且不包含的操作区间,
设 \(dp_{l, r, k, 0/1}\) 表示区间 \([l,r]\) 是否全部变成 \(k\) 的最小操作数 ,
\(dp_{l, r, k, 0} = \min\{\min\{dp_{l, mid, k, 0} + dp_{mid + 1, r, k, 0}\}, \min\{dp_{l, r, p, 0}\} + 1\}\),
\(dp_{l, r, k, 1} = \min\{\min\{dp_{l, mid, k, 1} + dp_{mid + 1, r, k, 1}\}, dp_{l, r, k, 0} + 1\}\)。
第一个转移方程看上去可能会发生自转移,但是右边的 \(\min\) 只会由最小的一个转移出去,且它不会被其他状态更新。
CF981D Bookshelves *1900,贪心,DP
显然是先按位贪心,然后 DP 求出能否达到当前答案。
CF1915G Bicycles *1800,最短路
这是一类最短路建模题,需要在状态记录上面做改动。
如果直接跑一维的 Dijkstra 是不行的,因为可以先到达一个速度较小的城市获取它的速度,然后再返回。考虑增加状态。
设 \(f_{i, j}\) 表示到达 \(i\) 的最短路且当前速度为 \(j\),贪心地,我们到达一个点时肯定要选速度较小的一个,于是在 Dijkstra 松弛的时候,有转移 \(f_{u, i} + i \times w_{u, v} \to f_{v, \min\{i, a_v\}}\)。
CF1714D Color with Occurrences *1600,DP
一时脑抽,不会做 *1600,真的唐完了。
设 \(f_i\) 表示填满前 \(i\) 个需要的最少步数,直接暴力枚举转移即可。
ABC373F Knapsack with Diminishing Values *2018,背包DP
好题。
第一眼想到的是多重背包,用单调队列优化,但是发现 \(kv - k^2\) 是个凸函数,不是单调的,所以不行。(可以决策单调性优化,但是当时不会)。
这种式子肯定是想办法拆贡献了。发现 \(kv - k^2 = (k-1)v-(k-1)^2 + (v-2k+1)\),也就是说,每多一个相同种类的物品,贡献就会加上 \((v-2k+1)\),(当前是第 \(k\) 个)。那么每次的贡献都是上一次的贡献 \(-2\),然后第一次的贡献是 \(v-1\)。
朴素的想法是设 \(f_{i, j}\) 表示前 \(i\) 个物品中容量为 \(j\) 的最大答案,但是不加优化只能 \(O(n^3)\)。
那么我们考虑如何优化状态,先把物品按照重量分组,然后设 \(f_{i,j}\) 表示重量小于等于 \(i\) 的物品中,容量为 \(j\) 能获得的最大价值,其实也是背包。
接着需要预处理出 \(g_{i}\) 表示当前重量的物品中,拿 \(i\) 个能获得的最大价值。\(g\) 可以贪心取,所以可以用优先队列维护,具体来说,假如我们考虑的是重量为 \(w\) 这一组,那么对于每个物品的价值 \(v\),把 \(v-1\) 放入队列,然后每次都选队头 \(x\) (最大值),弹出后把 \(x - 2\) 再放入队列。
设 \(N, W\) 同阶。总时间复杂度 \(O(N^2\log N)\)。\(O(N\log N)\) 是转移 \(g\) 时需要优先队列。转移 \(f\) 是 \(O(\sum \frac{i}{n})\),这个应该是近似与 \(O(N)\) 的。
P2943 [USACO09MAR] Cleaning Up G *提高+/省选−,DP
设 \(f_{i}\) 表示 \([1, i]\) 的最小答案,朴素转移是 \(O(n^2)\) 的。
但是,最终答案的其中一段的不同的数的个数不会超过 \(\sqrt n\)。那么我们可以维护一个链表,假设当前遍历到 \(i\),链表为 \((i-5)\to (i-3)\to (i-1)\to (i)\),这个链表的实际意义是:\([i, i-1]\) 中不同数的个数为 \(2\), \([i,i-5]\) 中不同数个数为 \(4\),依此类推。于是对于 \(f_{i}\),我们就从 \(f_{i-1} + 1\times 1\),\(f_{i-3}+2\times 2\),等等,往前跳 \(\sqrt n\) 个,如此转移过来。然后每次只需要把上一个和 \(i\) 相同的数从链表中删去,再把 \(i\) 从后面加入链表,即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int N = 2e5 + 5;
const ll inf = 1e18;
int n, m;
int pos[N];
ll a[N], b[N], dp[N];
int pre[N], nex[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i];
}
int k = n;
sort(b + 1, b + 1 + k);
k = unique(b + 1, b + 1 + k) - (b + 1);
for (int i = 1; i <= n; i++) {
a[i] = lower_bound(b + 1, b + 1 + k, a[i]) - b;
pre[i] = i - 1;
nex[i - 1] = i;
if (pos[a[i]]) {
nex[pre[pos[a[i]]]] = nex[pos[a[i]]];
pre[nex[pos[a[i]]]] = pre[pos[a[i]]];
}
pos[a[i]] = i;
ll cnt = 1;
int now = i;
dp[i] = inf;
while (now && cnt * cnt <= n) {
now = pre[now];
dp[i] = min(dp[i], dp[now] + cnt * cnt);
cnt++;
}
}
cout << dp[n] << "\n";
return 0;
}
ABC381F 1122 Subsequence *1739,状压DP
设 \(f_{S}\) 表示当前合法串由集合 \(S\) 中的数构成时的最后一个数的最小位置,
设 \(T_{i} = S - \{a_i\}\),显然 \(\min\{f_{T_i}\} \to f_S\)。
预处理 \(g_{i, j}\) 表示在 \(i\) 后面最近的一个 \(j\) 出现的位置。
于是 \(f_S = \min\{g(g(f_{T_i}, a_i), a_i)\}\)。集合的记录用状压即可。
CF1030E Vasya and Good Sequences *2000,计数
一个序列异或和为 \(0\) 说明这个序列中每一个二进制位上是 \(1\) 的数的个数都是偶数。
那么这些数 \(1\) 的个数总和也是偶数,并且 \(1\) 的个数最大值不能超过总数的一半。
考虑枚举右端点,设 \(sum\) 为前缀和,用桶记录前面 \((sum\mod 2)\) 的个数。
第一个限制条件可以用前缀和计算,对于不满足第二个限制条件,再把它减去。
由于每个数都至少会提供一个 \(1\),那么,只需要往前找 \(63\) 个数即可,因为超过 \(63\) 个数不可能出现违反限制二的情况。
P3959 [NOIP2017 提高组] 宝藏 *省选/NOI−,状压DP
设 \(f_{i, j, S}\) 表示当前子树的根为 \(j\),根的深度为 \(i\),子树 (不包括 \(j\)) 的点集为 \(S\) 的最小答案。
\(f_{i, j, S} = \min\{f_{i+1,k,S'} + i \times w_{j, k} + f_{i, j, S - S'}\}\),\(k \in S'\),\(S' \subset S\)。
答案为 \(\min\{f_{1,i,S - \{i\}}\}\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int N = 12 + 5, M = 1e3 + 5, S = (1 << 12) + 5;
const ll inf = 0x3f3f3f3f3f3f3f3f;
int n, m, bit[N], cnt[S];
ll G[N][N], dp[N][N][S];
void getmin(ll &x, ll y) { x = min(x, y); }
void init() {
memset(G, 0x3f, sizeof(G));
memset(dp, 0x3f, sizeof(dp));
}
int main() {
init();
cin >> n >> m;
int u, v; ll w;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
getmin(G[u][v], w);
getmin(G[v][u], w);
}
int tot = (1 << n) - 1;
bit[0] = 1;
for (int i = 1; i <= n; i++) {
bit[i] = bit[i - 1] << 1;
}
for (int i = 1; i <= tot; i++) {
cnt[i] = cnt[i - (i & -i)] + 1;
}
for (int dep = n; dep >= 1; dep--) {
for (int i = 1; i <= n; i++) {
dp[dep][i][0] = 0;
}
}
for (int dep = n - 1; dep >= 1; dep--) {
for (int cur = 1; cur <= tot; cur++) {
for (int u = 1; u <= n; u++) {
if ((cur & bit[u - 1]) || (n - cnt[cur] < dep)) continue;
for (int to = cur; to; to = (to - 1) & cur) {
for (int v = 1; v <= n; v++) {
if (!(to & bit[v - 1]) || G[u][v] == inf) continue;
getmin(dp[dep][u][cur], dp[dep + 1][v][to ^ bit[v - 1]] + dep * G[u][v] + dp[dep][u][cur - to]);
}
}
}
}
}
ll ans = inf;
for (int i = 1; i <= n; i++) {
getmin(ans, dp[1][i][tot ^ bit[i - 1]]);
}
cout << ans << '\n';
return 0;
}
P3953 [NOIP2017 提高组] 逛公园 *省选/NOI−,图上DP
设 \(d_{i, j}\) 表示 \(i\) 到 \(j\) 的最短路,\(p_{i, j}\) 表示 \(i\) 到 \(j\) 某条路径长度,\(w_{i, j}\) 表示边 \((i\to j)\),\(f_{u, i}\) 表示 \(p_{1, u} = d_{1, u} + i\) 的路径数量。
转移有,\(f_{u, i}\to f_{v, d_{1, u} + i + w_{i,j} - d_{1, v}}\)。
答案,\(\sum_{i\in [0, k]} f_{n, i}\)。
要按照 \(d_{1, u}\) 从小到大排序转移。
但是这样处理不了 \(0\) 边,可以将 \(0\) 边提出来建新图,按照拓扑序为第二关键字排序。
转移先枚举第二维,然后按照排序顺序,对于每个点从它的出边向外转移。
对于 \(-1\) 的情况,在拓扑排序后可以判断有没有在 \(0\) 环上的点,假如是 \(u\),分两种情况判断,
-
\(d_{1, u}+d_{u, n}\le d_{1, n} + k\),\((u\in [2, n - 1])\)
-
\(d_{1, n} = d_{n, 1} = 0\),\((u \in \{1, n\})\)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
using pli = pair<ll, int>;
const int N = 1e5 + 5, K = 50 + 5;
const ll inf = 1e18;
int n, m;
ll k, mod;
ll dp[N][K]; // dp[i][j] : dis[1][i] == dis1[i] + k 的路径数量
vector<pair<int, ll>> G[N], rG[N]; // 原图,反图
vector<int> G0[N]; // 0边图
int in[N], ord[N], inx; // 入度,topo序
int id[N], pos[N];
ll dis1[N], disu[N], disn[N];
bool vis[N];
void dijkstra(int st, ll *dis, vector<pair<int, ll>> *G) {
priority_queue<pli, vector<pli>, greater<pli>> pq;
fill(dis + 1, dis + 1 + n, inf);
fill(vis + 1, vis + 1 + n, false);
dis[st] = 0;
pq.emplace(0, st);
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto e : G[u]) {
int v = e.first;
ll w = e.second;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
pq.emplace(dis[v], v);
}
}
}
}
void topo() {
queue<int> q;
for (int i = 1; i <= n; i++) {
if (in[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
ord[u] = ++inx;
for (int v : G0[u]) {
if (--in[v] == 0) q.push(v);
}
}
}
void init() {
memset(dp, 0, sizeof(dp));
memset(in, -1, sizeof(in));
inx = 0;
}
void clear(int n) {
for (int i = 1; i <= n; i++) {
G[i].clear();
rG[i].clear();
in[i] = -1;
G0[i].clear();
}
}
void Solve() {
init();
cin >> n >> m >> k >> mod;
int u, v; ll w;
for (int i = 1; i <= m; i++) {
cin >> u >> v >> w;
G[u].emplace_back(v, w);
rG[v].emplace_back(u, w); // 反图
}
// 最短路
dijkstra(1, dis1, G); // 1 到 u
dijkstra(n, disu, rG); // u 到 n
dijkstra(n, disn, G); // n 到 u
// 建0边图
for (int u = 1; u <= n; u++) {
for (auto e : G[u]) {
int v = e.first;
ll w = e.second;
if (w == 0) {
if (in[u] == -1) in[u] = 0;
if (in[v] == -1) in[v] = 0;
G0[u].push_back(v);
in[v]++;
}
}
}
// 对0边图拓扑排序
topo();
// 两种无解的情况
for (int i = 2; i < n; i++) {
if (in[i] > 0 && dis1[i] + disu[i] <= dis1[n] + k) {
cout << -1 << '\n';
clear(n);
return;
}
}
if (dis1[n] == 0 && disn[1] == 0) {
cout << -1 << '\n';
clear(n);
return;
}
// 排序
iota(id + 1, id + 1 + n, 1);
sort(id + 1, id + 1 + n, [&](int i, int j) { return dis1[i] == dis1[j] ? ord[i] < ord[j] : dis1[i] < dis1[j]; });
for (int i = 1; i <= n; i++) {
pos[id[i]] = i;
}
sort(dis1 + 1, dis1 + 1 + n);
// 转移
dp[1][0] = 1 % mod;
for (int i = 0; i <= k; i++) {
for (int j = 1; j <= n; j++) {
int u = id[j];
for (auto e : G[u]) {
int v = e.first;
ll w = e.second;
if (dis1[pos[u]] + i + w - dis1[pos[v]] <= k && dis1[pos[u]] + i + w - dis1[pos[v]] >= 0) {
dp[v][dis1[pos[u]] + i + w - dis1[pos[v]]] += dp[u][i];
dp[v][dis1[pos[u]] + i + w - dis1[pos[v]]] %= mod;
}
}
}
}
ll ans = 0;
for (int i = 0; i <= k; i++) ans = (ans + dp[n][i]) % mod;
cout << ans << '\n';
clear(n);
}
int main() {
int T;
cin >> T;
while (T--) Solve();
return 0;
}
P3960 [NOIP2017 提高组] 列队 *省选/NOI−,线段树
考虑用 \(n\) 棵线段树维护每一行的前 \(m-1\) 个元素,再用第 \(n + 1\) 棵线段树维护第 \(m\) 列所有元素。
维护其区间和,有元素的地方为 \(1\),没有则为 \(0\)。若要访问某棵线段树的第 \(k\) 个元素,只需要线段树上二分找前缀和为 \(k\) 的位置。
对于删除操作 \((x, y)\),把线段树 \(x\) 的第 \(y\) 个元素改为 \(0\),并记录其编号,然后将编号插入线段树 \(n + 1\) 的最后位置,再把线段树 \(n+1\) 的第 \(x\) 个元素删除,并插入线段树 \(x\) 的最后位置,即可。\(y = m\) 时特判。
由于 \(N\) 是 \(3\times 10^5\) 级别的,考虑动态开点,一共只有 \(Q\) 次询问,所以空间复杂度为 \(O(QlogN)\)。初始时每棵线段树的总区间设为 \([1, \max(n, m)+q]\) 即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int N = 3e5 + 5;
int n, m, q;
struct SegmentTree {
int tot, ls[N * 20], rs[N * 20], sum[N * 20];
ll id[N * 20];
ll query(int root, int &x, int l, int r, int k) {
if (!x) {
x = ++tot;
if (root == n + 1) {
if (r <= n) sum[x] = r - l + 1;
else if (l <= n) sum[x] = n - l + 1;
else sum[x] = 0;
} else {
if (r <= m - 1) sum[x] = r - l + 1;
else if (l <= m - 1) sum[x] = m - l;
else sum[x] = 0;
}
if (l == r) {
if (root == n + 1) id[x] = 1ll * l * m;
else id[x] = 1ll * (root - 1) * m + l;
}
}
sum[x]--;
if (l == r) return id[x];
int mid = l + r >> 1;
if ((!ls[x] && k <= mid - l + 1) || k <= sum[ls[x]]) return query(root, ls[x], l, mid, k);
else {
if (!ls[x]) k -= mid - l + 1;
else k -= sum[ls[x]];
return query(root, rs[x], mid + 1, r, k);
}
}
void update(int root, int &x, int l, int r, int k, ll v) {
if (!x) {
x = ++tot;
if (root == n + 1) {
if (r <= n) sum[x] = r - l + 1;
else if (l <= n) sum[x] = n - l + 1;
else sum[x] = 0;
} else {
if (r <= m - 1) sum[x] = r - l + 1;
else if (l <= m - 1) sum[x] = m - l;
else sum[x] = 0;
}
}
sum[x]++;
if (l == r) return void(id[x] = v);
int mid = l + r >> 1;
if (k <= mid) update(root, ls[x], l, mid, k, v);
else update(root, rs[x], mid + 1, r, k, v);
}
} sgt;
int root[N], len[N];
int main() {
cin >> n >> m >> q;
for (int i = 1; i <= n; i++) {
len[i] = m - 1;
}
len[n + 1] = n;
int _n = max(n, m) + q;
int x, y; ll id;
while (q--) {
cin >> x >> y;
if (y == m) {
id = sgt.query(n + 1, root[n + 1], 1, _n, x);
sgt.update(n + 1, root[n + 1], 1, _n, ++len[n + 1], id);
} else {
id = sgt.query(x, root[x], 1, _n, y);
sgt.update(x, root[x], 1, _n, ++len[x], sgt.query(n + 1, root[n + 1], 1, _n, x));
sgt.update(n + 1, root[n + 1], 1, _n, ++len[n + 1], id);
}
cout << id << '\n';
}
return 0;
}
CF2020D Connect the Dots *1800,并查集,根号分治
因为 \(d\le 10\),我们可以直接维护 \(f_{i,j}\) 表示从 \(i\) 开始,\(d=j\) 时,覆盖到的最远位置,最后再统一用并查集合并即可。
推广到 \(d\le n\) 的情况,考虑根号分治。对于 \(d\le \sqrt n\) 直接用上面的维护方法。对于 \(d> \sqrt n\),我们可以直接用并查集暴力合并,跳的次数是不超过 \(\sqrt n\) 的。总复杂度 \(O(n\sqrt n)\)。
CF1913D Array Collapse *2100,计数DP
设 \(f_{i,0/1}\) 表示考虑前 \(i\) 个元素,保留 \(a_i\) 或删除 \(a_i\) 的方案数。
设 \(p\) 表示 \(a_i\) 前面第一个比 \(a_i\) 小的数的位置。
转移:\(f_{i,0}=f_{p,1}+\sum_{j=p}^{i-1} f_{j,0}\),\(f_{i,1}=f_{p,0}+f_{p,1}\)。
\(p\) 用单调栈维护,求和用前缀和维护即可。
答案是 \(f_{n,0}+f_{n,1}\)。

浙公网安备 33010602011771号