忘掉过去的人,终将重蹈覆辙。|

chenwenmo

园龄:6个月粉丝:0关注:7

做题记录 (2024-11-21 to 2024-11-29)

CF1922F Replace on Segment *2500,区间DP

可证最优解可以不存在相交且不包含的操作区间,

dpl,r,k,0/1 表示区间 [l,r] 是否全部变成 k 的最小操作数 ,

dpl,r,k,0=min{min{dpl,mid,k,0+dpmid+1,r,k,0},min{dpl,r,p,0}+1}

dpl,r,k,1=min{min{dpl,mid,k,1+dpmid+1,r,k,1},dpl,r,k,0+1}

第一个转移方程看上去可能会发生自转移,但是右边的 min 只会由最小的一个转移出去,且它不会被其他状态更新。

Code


CF981D Bookshelves *1900,贪心,DP

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

Code


CF1915G Bicycles *1800,最短路

这是一类最短路建模题,需要在状态记录上面做改动。

如果直接跑一维的 Dijkstra 是不行的,因为可以先到达一个速度较小的城市获取它的速度,然后再返回。考虑增加状态。

fi,j 表示到达 i 的最短路且当前速度为 j,贪心地,我们到达一个点时肯定要选速度较小的一个,于是在 Dijkstra 松弛的时候,有转移 fu,i+i×wu,vfv,min{i,av}

Code


CF1714D Color with Occurrences *1600,DP

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

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

Code


ABC373F Knapsack with Diminishing Values *2018,背包DP

好题。

第一眼想到的是多重背包,用单调队列优化,但是发现 kvk2 是个凸函数,不是单调的,所以不行。(可以决策单调性优化,但是当时不会)。

这种式子肯定是想办法拆贡献了。发现 kvk2=(k1)v(k1)2+(v2k+1),也就是说,每多一个相同种类的物品,贡献就会加上 (v2k+1),(当前是第 k 个)。那么每次的贡献都是上一次的贡献 2,然后第一次的贡献是 v1

朴素的想法是设 fi,j 表示前 i 个物品中容量为 j 的最大答案,但是不加优化只能 O(n3)

那么我们考虑如何优化状态,先把物品按照重量分组,然后设 fi,j 表示重量小于等于 i 的物品中,容量为 j 能获得的最大价值,其实也是背包。

接着需要预处理出 gi 表示当前重量的物品中,拿 i 个能获得的最大价值。g 可以贪心取,所以可以用优先队列维护,具体来说,假如我们考虑的是重量为 w 这一组,那么对于每个物品的价值 v,把 v1 放入队列,然后每次都选队头 x (最大值),弹出后把 x2 再放入队列。

N,W 同阶。总时间复杂度 O(N2logN)O(NlogN) 是转移 g 时需要优先队列。转移 fO(in),这个应该是近似与 O(N) 的。

Code


P2943 [USACO09MAR] Cleaning Up G *提高+/省选−,DP

fi 表示 [1,i] 的最小答案,朴素转移是 O(n2) 的。

但是,最终答案的其中一段的不同的数的个数不会超过 n。那么我们可以维护一个链表,假设当前遍历到 i,链表为 (i5)(i3)(i1)(i),这个链表的实际意义是:[i,i1] 中不同数的个数为 2[i,i5] 中不同数个数为 4,依此类推。于是对于 fi,我们就从 fi1+1×1fi3+2×2,等等,往前跳 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

fS 表示当前合法串由集合 S 中的数构成时的最后一个数的最小位置,

Ti=S{ai},显然 min{fTi}fS

预处理 gi,j 表示在 i 后面最近的一个 j 出现的位置。

于是 fS=min{g(g(fTi,ai),ai)}。集合的记录用状压即可。

Code


CF1030E Vasya and Good Sequences *2000,计数

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

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

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

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

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

Code


P3959 [NOIP2017 提高组] 宝藏 *省选/NOI−,状压DP

fi,j,S 表示当前子树的根为 j,根的深度为 i,子树 (不包括 j) 的点集为 S 的最小答案。

fi,j,S=min{fi+1,k,S+i×wj,k+fi,j,SS}kSSS

答案为 min{f1,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

di,j 表示 ij 的最短路,pi,j 表示 ij 某条路径长度,wi,j 表示边 (ij)fu,i 表示 p1,u=d1,u+i 的路径数量。

转移有,fu,ifv,d1,u+i+wi,jd1,v

答案,i[0,k]fn,i

要按照 d1,u 从小到大排序转移。

但是这样处理不了 0 边,可以将 0 边提出来建新图,按照拓扑序为第二关键字排序。

转移先枚举第二维,然后按照排序顺序,对于每个点从它的出边向外转移。

对于 1 的情况,在拓扑排序后可以判断有没有在 0 环上的点,假如是 u,分两种情况判断,

  • d1,u+du,nd1,n+k(u[2,n1])

  • d1,n=dn,1=0(u{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 棵线段树维护每一行的前 m1 个元素,再用第 n+1 棵线段树维护第 m 列所有元素。

维护其区间和,有元素的地方为 1,没有则为 0。若要访问某棵线段树的第 k 个元素,只需要线段树上二分找前缀和为 k 的位置。

对于删除操作 (x,y),把线段树 x 的第 y 个元素改为 0,并记录其编号,然后将编号插入线段树 n+1 的最后位置,再把线段树 n+1 的第 x 个元素删除,并插入线段树 x 的最后位置,即可。y=m 时特判。

由于 N3×105 级别的,考虑动态开点,一共只有 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,并查集,根号分治

因为 d10,我们可以直接维护 fi,j 表示从 i 开始,d=j 时,覆盖到的最远位置,最后再统一用并查集合并即可。

推广到 dn 的情况,考虑根号分治。对于 dn 直接用上面的维护方法。对于 d>n,我们可以直接用并查集暴力合并,跳的次数是不超过 n 的。总复杂度 O(nn)

Code


CF1913D Array Collapse *2100,计数DP

fi,0/1 表示考虑前 i 个元素,保留 ai 或删除 ai 的方案数。

p 表示 ai 前面第一个比 ai 小的数的位置。

转移:fi,0=fp,1+j=pi1fj,0fi,1=fp,0+fp,1

p 用单调栈维护,求和用前缀和维护即可。

答案是 fn,0+fn,1

Code

本文作者:chenwenmo

本文链接:https://www.cnblogs.com/chenwenmo/p/18561836

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   chenwenmo  阅读(3)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起