洛谷P7842 探险者笔记 III 题解

题面

传送门
《探险者笔记》由 \(n\) 个关卡组成,每个关卡有一个难度 \(b_i\),通关顺序没有要求。同时有 \(m\) 个成就,第 \(i\) 个成就需要你恰好完成 \(sum_i\) 个关卡,且刚好分别是 \(a_{i_1},a_{i_2},...,a_{i_{sum_i}}\)。完成第 \(i\) 个成就可以得到 \(v_i\) 的分数。而且成就的解锁需要一定的顺序。上一个获得第 \(i\) 个成就接下来获得第 \(j\) 个成就的条件是 \(i<j\)\(w+\sum_{k=1}^{sum_i} b_{a_{i_{k}}}≥\sum_{k=1}^{sum_j} b_{a_{j_{k}}}\),其中 \(w\) 是一个给定的常数。
第一次获得成就没有限制。求最多他能得到多少分数。
\(1≤n≤18\)\(1≤m≤10^5\)

题解

此题中对二维偏序的应用刷新了我的认知,似乎可以形成一种具有一定规律的DP优化技巧。

题目描述很冗长。因为 \(n≤18\),故用一个二进制数 \(p\) 表示需要完成的关卡。另外,用 \(c_i\) 表示 \(\sum_{k=1}^{sum_i} b_{a_{i_{k}}}\)
首先,此题显然可以用 \(O(N^2)\) 的DP实现。设 \(f_i\) 表示 \(1\)~\(i\) 能获得最多的分数,则

\[f_i=\max_{j=1}^{i-1} f_j+v_i (c_j+w≥c_i,p_j∈p_i) \]

考虑对其进行优化。
注意到条件 \(i>j\)\(c_j+w≥c_i\) 构成二维偏序,那么可以用CDQ分治加速判断 \(j\) :先按编号排序,在归并时按照 \(c\) 排序,双指针扫描,可以 \(O(m\log{m})\) 满足这两个条件。
再看另一个限制条件 \(p_j∈p_i\)。60pts中 \(n≤9\),这可以给我们启发,即依据每个 \(p_i\) 暴力枚举所有 \(p_j\)。那么复杂度为 \(O(2^nm\log{m})\)
进一步优化 \(n≤18\) 的情况。考虑上述暴力的两种方法:

  1. 左段中加入一个数(设其为 \(p_j,f_j\))时,枚举 \(p_j\) 所有超集,用 \(f_j\) 更新最大值。此法 \(O(2^n)\) 插入,\(O(1)\) 查询。
  2. 右段查询一个数(设为\(p_i\))时,枚举 \(p_i\) 所有子集,用其中的最大值+ \(v_i\) 更新 \(f_i\)。此法 \(O(1)\) 插入,\(O(2^n)\) 查询。

考虑将二者结合起来。将一个18位二进制数劈成两个9位二进制数,那么原数 \(x\) 子集中的任意数,劈成两半后一定分别为修改后两个 \(x'\) 的子集。于是我们可以将 \(O(2^n)\) 拆为 \(O(2×2^{n/2})\)(PS:这种时间复杂度并不严谨,但可以很好地表示出转化的过程)。具体来说,用一个二元组 \((x,y)\) 表示数 \((x<<9)+y\),左段中加入一个数时,枚举 \(x\) 所有超集,\(y\) 不变;右段中查询一个数时,枚举 \(y\) 所有子集,\(x\) 不变。总时间复杂度为 \(O(2^{n/2}m\log{m})\)

另外,关于CDQ分治中的细节。此题中CDQ分治是为DP服务,那么就要保证状态从 \(j\)\(i\) 时,\(j\) 一定已经有最优解。故应先 \(CDQ(l,mid)\),将状态转移给 \([mid+1,r]\) 后,再 \(CDQ(mid+1,r)\)

Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 20, M = 1e5 + 5, S = 1 << 9;
int n, m, w, b[N], g[S][S], ans;
struct Node {
    int id, p, c, v, res;
} q[M];
int read() {
    int x = 0; char c = getchar();
    while (c < '0' || c > '9') c = getchar();
    while (c >= '0' && c <= '9') {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();}
    return x;
}
bool cmp1(Node x, Node y) {return x.id < y.id;}
bool cmp2(Node x, Node y) {return x.c > y.c;}
void Add(int k, int v) {
    int l = k >> 9, r = (1 << 9) - 1 & k;
    if (n == 9) {
        g[0][r] = v ? max(g[0][r], v) : 0; return ; //n=9时无需枚举l的超集,小优化
    }
    for (int i = 0; i < 1 << 9; i++)
        if ((i & l) == l) g[i][r] = v ? max(g[i][r], v) : 0;
}
int Query(int k) {
    int l = k >> 9, r = (1 << 9) - 1 & k, res = 0;
    for (int i = 0; i < 1 << 9; i++)
        if ((i & r) == i) res = max(res, g[l][i]);
    return res;
}
void CDQ(int l, int r) {
    if (l == r) return ;
    int mid = l + r >> 1;
    CDQ(l, mid);
    sort(q + l, q + mid + 1, cmp2); sort(q + mid + 1, q + r + 1, cmp2);
    int i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i].c + w >= q[j].c) Add(q[i].p, q[i].res), i++;
        else q[j].res = max(q[j].res, Query(q[j].p) + q[j].v), j++;
    while (j <= r) q[j].res = max(q[j].res, Query(q[j].p) + q[j].v), j++;
    for (i = l; i <= mid; i++) Add(q[i].p, 0);
    sort(q + mid + 1, q + r + 1, cmp1); //递归右段前先保证编号有序
    CDQ(mid + 1, r);
}
int main() {
    n = read(); m = read(); w = read();
    for (int i = 1; i <= n; i++) b[i] = read();
    for (int i = 1; i <= m; i++) {
        q[i].res = q[i].v = read(); q[i].id = i;
        int s = read();
        while (s--) {
            int x = read();
            q[i].c += b[x]; q[i].p |= 1 << x - 1;
        }
    }
    CDQ(1, m);
    for (int i = 1; i <= m; i++) ans = max(ans, q[i].res);
    printf("%d\n", ans);
    return 0;
}

此题中利用CDQ分治优化DP的方式似乎具有普适性,可以形成类似单调队列优化DP的一种思考方向。

posted @ 2022-04-13 19:11  realFish  阅读(62)  评论(0)    收藏  举报