做题记录 (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\) 只会由最小的一个转移出去,且它不会被其他状态更新。

Code


CF981D Bookshelves *1900,贪心,DP

显然是先按位贪心,然后 DP 求出能否达到当前答案。

Code


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\}}\)

Code


CF1714D Color with Occurrences *1600,DP

一时脑抽,不会做 *1600,真的唐完了。

\(f_i\) 表示填满前 \(i\) 个需要的最少步数,直接暴力枚举转移即可。

Code


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)\) 的。

Code


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)\}\)。集合的记录用状压即可。

Code


CF1030E Vasya and Good Sequences *2000,计数

一个序列异或和为 \(0\) 说明这个序列中每一个二进制位上是 \(1\) 的数的个数都是偶数。

那么这些数 \(1\) 的个数总和也是偶数,并且 \(1\) 的个数最大值不能超过总数的一半。

考虑枚举右端点,设 \(sum\) 为前缀和,用桶记录前面 \((sum\mod 2)\) 的个数。

第一个限制条件可以用前缀和计算,对于不满足第二个限制条件,再把它减去。

由于每个数都至少会提供一个 \(1\),那么,只需要往前找 \(63\) 个数即可,因为超过 \(63\) 个数不可能出现违反限制二的情况。

Code


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)\)

Code


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}\)

Code

posted @ 2024-11-22 00:36  chenwenmo  阅读(18)  评论(0)    收藏  举报