动!态!规!划!10!题!
2024-09-06
DP

现在是 10 题。虽然看着很掉价,但是写够了 10 就改成 20 题。以此类推,免得我摆烂()

自用向,不写太详细,把破题点写出来就差不多了。哦哦这里的破题是动词不是名词。


1. 字符合并

https://www.luogu.com.cn/problem/P3736

状压区间 DP

注意到 \(k\) 只有 \(8\),独特的输入方式也引导我们注意到状态数为 \(10^2\) 级别。合并的背景又让我们本能想到区间 DP,所以刚好盲猜这题是个 \(O(2^k\times n^2)\) 的区间状压 DP。

  • 然后我觉得最神的一个地方是什么呢?最后的答案一定是一个长度小于 \(k\) 的串,每位展开还原,可以知道,每个合并操作选取的区间互不相交。

    看起来很蠢很显然,但这是我们 \(n^2\) 区间 DP 的基础啊(((

然后我们区间 DP 套路,枚举对于 \([l,r]\) 最左边一位,然后就可以开始大力转移了。

但是这里我们会发现,这一位原本的样子有点多,可以任何一个 \(1 + d\times (k-1)\),所以只能再来一层 \(\dfrac nk\) 地枚举,极限情况是个有点难绷的 3e9,作为正解只能说这个数据范围的提示性有点令人难评了。

本来我实现着实现着把自己整懵了:000000 压出来都是 0,但二者代表的含义显然不同,又无法简单区别。但其实长度为 \(1+d\times (k-1)\) 的区间和长度为 \(d\) 的最终结果是一一对应的,所以在我们设计的包含了区间作右端点的状态中,压出来的就是我们想要的那个。

这里的实现比所有我找到的题解都更像人打出来的。导致我想对比疗法都找不到合适的代码

换言之,这最像是一份通常的区间 DP 代码,不存在其他题解中乱七八糟的填表啊相互影响啊之类的恶心 feature。

sto rybp orz

#include <bits/stdc++.h>
const int maxn = 305;
const int maxm = (1 << 8) + 5;
char a[maxn];
int c[maxm], w[maxm];
long long f[maxn][maxn][maxm];
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
#else
    freopen("in.in", "r", stdin);
    freopen("out.out", "w", stdout);
#endif
    int n, k;
    std::cin >> n >> k;
    int siz = (1 << k), si = (1 << (k - 1));
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i];
    for (int i = 0; i < siz; ++i)
        std::cin >> c[i] >> w[i];
    memset(f, -0x3f, sizeof(f));
    for (int l = 1; l < k; ++l)
        for (int i = 1; i <= n - l + 1; ++i) {
            int j = i + l - 1, now = 0;
            for (int p = i; p <= j; ++p)
                now = now * 2 + a[p] - '0';
            f[i][j][now] = 0;
        }
    for (int i = 1; i <= n - k + 1; ++i) {
        int now = 0;
        for (int j = i; j <= i + k - 1; ++j)
            now = now * 2 + a[j] - '0';
        f[i][i + k - 1][c[now]] = w[now];
    }
    for (int l = k + 1; l <= n; ++l)
        for (int i = 1; i <= n - l + 1; ++i) {
            int j = i + l - 1;
            for (int p = i; p < j; p += k - 1) {
                for (int q = 0; q < si; ++q)
                    if (!(q >> ((l - 1) % (k - 1) + 1)))
                        f[i][j][q] = std::max(f[i][j][q], f[i][p][q >> ((l - 1) % (k - 1))] + f[p + 1][j][q & ((1 << ((l - 1) % (k - 1))) - 1)]);
                if (l % (k - 1) == 1 || k == 2)
                    for (int q = 0; q < siz; ++q)
                        f[i][j][c[q]] = std::max(f[i][j][c[q]], f[i][p][q >> (k - 1)] + f[p + 1][j][q & ((1 << (k - 1)) - 1)] + w[q]);
            }
        }
    std::cout << *std::max_element(f[1][n], f[1][n] + si) << '\n';
    return 0;
}

其实一开始挺担心会不会因为写博客影响做题时间之类的。

后来发现我可以看知乎看一个上午然后代码动都不动一下,反正不想做题的时候也不能强制让自己做题,那不如写写博客算了


2. Mod Mod Mod

https://codeforces.com/contest/889/problem/E

技巧 / 特殊性质类 DP

小神题。注意到题意可以转化为,找到一个 \(x\),最大化:

\[ x\bmod a_1 + x\bmod a_1\bmod a_2 + \cdots + x\bmod a_1\bmod a_2\bmod\cdots\bmod a_n. \]

简称 \(x\bmod a_1\bmod a_2\bmod \cdots\bmod a_i\) 的值为 \(f_i\)

这个时候我们不难注意到,\(f_i\) 肯定是单调不升的。形象化地,整个 \(f\) 序列肯定是由若干个单调下降的段组成的。并且,必定存在至少一个 \(i\),使得 \(f_i = a_i-1\),否则可以将所有 \(f_i\gets f_i+1\),同时 \(\sum f\gets (\sum f) + n\)

这启示我们关注 \(f_i\) 的值域。假设已经确定了 \(f\) 序列的一个前缀 \(f_1\sim i\),对于任意 \(0\le k\le f_{\min}\)(注意由于单调不降,\(f_{\min} = f_i\)),我们可以将所有 \(f_j\gets f_j-k\),得到一个新的合法的 \(f'_{1\sim i}\)

形象化地,将 \(f_{1\sim i}\) 想象为一个逐渐降低的柱状图,任意切去了一个高度的底部,并且这个高度不高于最矮的元素(即第 \(i\) 个元素)。

这时候有一个很美妙的性质,就是我们对于切之前和切之后的柱状图,其 最矮元素以上的部分 长得完全相同。然后就是神中神之 DP 状态设计。很难想象是在什么样的精神状态下凑出来这种神奇状态的,可能是某种我不了解的 trick?

\(dp_{i, j}\) 表示对于前缀 \(f_{1, i}\),当切去的高度为 \(0\sim j\) 时,最矮元素以上的部分 的和都为 \(dp_{i, j}\)。那么易得 \(\sum f_{1\sim i}=j\times i + dp_{i, j}\)

考虑转移。假设已知所有 \(dp_{i - 1, j}\),需转移到 \(i\),我们有三种转移路径:

  1. \(j < a_i\)

    \(a_i\) 取模取不动,不会产生任何影响。\(dp_{i, j}\gets dp_{i - 1, j}\)

  2. \(j\ge a_i\)

    • \(i\) 承担 \(f_i=a_i-1\) 的责任。我们从 \(0\sim j\) 中找到一个满足 \(j'\bmod a_i=a_i-1\)\(j'\)\(j'\) 肯定是越大越好,故 \(j'=(\left\lfloor \dfrac {j}{a_i}\right\rfloor-1)\times a_i+(a_i-1)\)

      此时便有 \(dp_{i, a_i-1}\gets dp_{i - 1, j'} + [j' - (a_i-1)]\times (i-1)\)

    • 盲猜 \(0\sim i - 1\)\(i+1\sim n\) 中会出现承担 \(f_{i'}=a_{i'-1}\)\(i'\),故按照 \(j\) 取最大来转移。

      此时有 \(dp_{i, j\bmod a_i}=dp_{i-1,j}+(j-j\bmod a_i)\times (i-1)\)

答案即为 \(\max\{j\times n+dp_{n, j}\}\)。由于不存在 \(f_i=a_i-1\)\(f\) 肯定比存在的要劣,所以我们肯定选到的是正确的答案。

注意到每个 \(i\) 只会新增 \(a_i-1\) 一个状态,故状态总数为 \(O(n)\)。把 \(dp\) 开成 map 就可以 \(O(n\log n)\) 地解决问题。

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
#else
    freopen(".in", "r", stdin);
    freopen(".out", "w", stdout);
#endif
    int n;
    std::cin >> n;
    std::vector<long long> a(n + 1);
    std::map<long long, long long> t;
    std::cin >> a[1];
    t[a[1] - 1] = 0;
    for (int i = 2; i <= n; ++i) {
        std::cin >> a[i];
        for (;;) {
            auto p = t.lower_bound(a[i]);
            if (p == t.end())
                break;
            long long j = p->first, f = p->second;
            t[j % a[i]] = std::max(t[j % a[i]], f + (j - j % a[i]) * (i - 1));
            if (j >= a[i]) {
                j = (j / a[i] - 1) * a[i] + a[i] - 1;
                t[a[i] - 1] = std::max(t[a[i] - 1], f + (j - (a[i] - 1)) * (i - 1));
            }
            t.erase(p);
        }
    }
    long long res = 0;
    for (auto i : t)
        res = std::max(res, i.first * n + i.second);
    std::cout << res << '\n';
    return 0;
}

3. StalinSort Algorithm

https://qoj.ac/problem/1456

你会发现删最少 = 留最多

考虑 \(i\) 能够转移到的 \(j(j>i)\) 需满足的条件。若令 \(nex_p\) 表示 \(p\) 之后第一个值比 \(a_p\) 大的元素,则 \(j\in [nex_i, nex_{nex_i})\)\(a_j>a_i\)

赛时误认为右端点为 \(i\) 之后第二个值比 \(a_i\) 大的元素,导致完全寄掉。

时间复杂度 \(O(n^2)\),考虑优化。注意到如果只有 \(j\in [nex_i, nex_{nex_i})\) 这个条件很容易用线段树维护,考虑消去 \(a_j>a_i\) 的影响。故考虑按 \(a\) 从小到大 的顺序选取 \(i\),因为当前最小的 \(i\) 一定已经被刷完表了

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

#include <bits/stdc++.h>
const int maxn = 5e5 + 5;
const int inf = 0x3f3f3f3f;
struct _ { int u, d; } t1[maxn << 2];
int n, t[maxn << 2];
#define lt (p << 1)
#define rt (lt | 1)
void upd(int p, int l, int r, int x, int v) {
    t[p] = std::min(t[p], v);
    if (l == r)
        return;
    int mid = (l + r) >> 1;
    if (x <= mid)
        upd(lt, l, mid, x, v);
    else
        upd(rt, mid + 1, r, x, v);
    return;
}
int ask(int p, int l, int r, int ql, int qr) {
    if (ql <= l && r <= qr)
        return t[p];
    int mid = (l + r) >> 1, res = n + 1;
    if (ql <= mid)
        res = ask(lt, l, mid, ql, qr);
    if (qr > mid)
        res = std::min(res, ask(rt, mid + 1, r, ql, qr));
    return res;
}
void pushdown(int p) {
    if (t1[p].d) {
        t1[lt].u = std::max(t1[lt].u, t1[p].d);
        t1[lt].d = std::max(t1[lt].d, t1[p].d);       
        t1[rt].u = std::max(t1[rt].u, t1[p].d);
        t1[rt].d = std::max(t1[rt].d, t1[p].d);       
        t1[p].d = 0;
    }
    return;
}
void upd(int p, int l, int r, int ql, int qr, int v) {
    t1[p].u = std::max(t1[p].u, v);
    if (ql <= l && r <= qr) {
        t1[p].d = std::max(t1[p].d, v);
        return;
    }
    pushdown(p);
    int mid = (l + r) >> 1;
    if (ql <= mid)
        upd(lt, l, mid, ql, qr, v);
    if (qr > mid)
        upd(rt, mid + 1, r, ql, qr, v);
    return;
}
int ask(int p, int l, int r, int x) {
    if (l == r)
        return t1[p].u;
    pushdown(p);
    int mid = (l + r) >> 1;
    if (x <= mid)
        return ask(lt, l, mid, x);
    return ask(rt, mid + 1, r, x);
}
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
#else
    freopen("sort3.in", "r", stdin);
    // freopen(".out", "w", stdout);
#endif
    int res = 0;
    std::cin >> n;
    std::vector<int> a(n + 1), ne(n + 1), f(n + 1);
    std::fill(t + 1, t + 4 * n + 1, n + 1);
    std::fill(t1 + 1, t1 + 4 * n + 1, (_){ -inf, 0 });
    for (int i = 1; i <= n; ++i)
        std::cin >> a[i];
    for (int i = n; ~i; --i) {
        ne[i] = ask(1, 0, n, a[i] + 1, n);
        upd(1, 0, n, a[i], i);   
    }
    std::vector<int> id(n + 1);
    std::iota(id.begin(), id.end(), 0);
    std::sort(id.begin(), id.end(), [&](int x, int y) { return a[x] < a[y]; });
    f[0] = 0;
    for (int i = 0; i <= n; ++i) {
        if (i) {
            f[id[i]] = ask(1, 0, n, id[i]);
            res = std::max(res, f[id[i]]);
        }
        if (ne[id[i]] <= n)
            upd(1, 0, n, ne[id[i]], ne[ne[id[i]]] - 1, f[id[i]] + 1);
    }
    std::cout << n - res << '\n';
    return 0;
}

4. Merge Triplets

https://atcoder.jp/contests/agc043/tasks/agc043_d

由于要对合法的最终序列进行计数,考虑最终序列的性质。

若序列中 \(a_{i+1}<a_i\),则说明在某一组中,值为 \(a_i\) 的元素下一个就是 \(a_{i+1}\)。否则,因 \(a_{i+1}\) 可选且比 \(a_i\) 小,\(a_{i+1}\) 应排在 \(a_i\) 前面。

否则,\(a_{i+1}\) 既可以与 \(a_i\) 不在同一组,也可以是 \(a_i\) 的下一个元素。

一组最多三个元素,也就是说,不能出现 \(a_i>a_{i+1},a_{i+2},a_{i+3}\) 的情况。而 \(N=1\) 时序列长度只有 \(3\),这就是为什么样例一的答案为 \(3!\)

但这个限制并不能排除所有非法情况。\(N=2\) 时暴搜我们目前限制下的解,共有 276 个,比答案中的 261 个要多,说明包含了其他非法解。

暴搜输出并观察,易得这 15 个非法序列即为满足目前限制且存在 \(3\)\(i\),满足 \(a_i>a_{i+1}\)\(a_i<a_{i+2}\) 的所有序列,由前面的推导,必须分为三个长度为 2 的组,而我们要求的是长度为 3 的组。显然无法用前者组合得到后者

在我们第一个条件的限制下,必须被分到同一组的数的连续长度,只有 \(1,2,3\) 三种。而:

  • 长度为 3 的对答案合法性无影响;
  • 而长度为 1 的既可以和 2 组为一组,也可以和其他两个 1 组为一组;
  • 长度为 2 的只能和 1 组成一组。

这时,我们发现所有限制都指向长度为 2 的段,其总个数不能比长度为 1 的段多。

于是乎,上述限制可总结为两点:

  1. 若令所有满足 \(a_l>\max\{a_{l\sim r}\}\) 的极大区间为一段(易证每段互不重合且覆盖全序列),那么其长度不能超过 3。
  2. 其中,长度为 2 的个数不能比长度为 1 的个数多。

\(f_{i, j}\) 表示若当前枚举的最后一段右端点为 \(i\),长度为 1 的段比长度为 2 的段多 \(j\) 的方案数。

最后答案即为 \(\sum\limits_{j=0}^n f_{i, j}\)

#include <bits/stdc++.h>
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
#else
    freopen(".in", "r", stdin);
    freopen(".out", "w", stdout);
#endif
    int n, mod;
    std::cin >> n >> mod;
    n *= 3;
    std::vector<std::unordered_map<int, long long> > f(n + 1);
    f[0][0] = 1;
    for (int i = 0; i < n; ++i)
        for (auto [j, k] : f[i]) {
            (f[i + 1][j + 1] += k) %= mod;
            if (i + 2 <= n)
                (f[i + 2][j - 1] += k * (i + 1) % mod) %= mod;
            if (i + 3 <= n)
                (f[i + 3][j] += k * (i + 1) % mod * (i + 2) % mod) %= mod;
        }
    long long res = 0;
    for (int i = 0; i <= n; ++i)
        (res += f[n][i]) %= mod;
    std::cout << res << '\n';
    return 0;
}

5. Runaway Quail

https://qoj.ac/problem/5978

容易发现鸡的奔跑方向从始至终不会变,如果我们要从往左追变成往右追,显而易见地我们会在追上右边第一只鸡前经过原点,反之同理。

那么有一个很神奇的状态设计,设 \(f_{i,j}\) 为追上了左边速度第 \(1\sim i - 1\) 大的所有鸡和右边速度第 \(1\sim j - 1\) 大的所有鸡,然后跑回原点的最小时间,这样我们就不用记录这个非常难记录的当前位置信息,对于速度排名 \(\ge i,j\) 的所有鸡,当前是否追上我们并不关心——如果其位置比较远,那么我们会在后续转移中再考虑;如果其位置比较近,在解决速度排名 \(<i,j\) 的鸡时就已经抓住

我们 \(\mathcal O(n)\) 枚举上一次反向是在抓住哪一只鸡时,然后让中间全部同向跑即可

也就是说,假设我们要用 \(f_{i,j}\) 更新 \(f_{k,j}\)\(k<i\)),那么只需要更新抓住速度排名 \(i\sim k+1\) 中跑得最远的一只鸡所需额外时间。按速度排序后可以线性算代价。

#include <bits/stdc++.h>
const long double eps = 1e-12;
int main() {
#ifdef ONLINE_JUDGE
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr), std::cout.tie(nullptr);
#else
    std::freopen(".in", "r", stdin);
    std::freopen(".out", "w", stdout);
#endif
    int T;
    for (std::cin >> T; T--; ) {
        int v, n;
        std::cin >> v >> n;
        std::vector<std::pair<int, int> > a(n + 1);
        for (int i = 1; i <= n; ++i)
            std::cin >> a[i].first;
        for (int i = 1; i <= n; ++i)
            std::cin >> a[i].second;
        a.emplace_back(0, 0), ++n;
        std::sort(a.begin() + 1, a.end());
        int p = std::lower_bound(a.begin() + 1, a.end(), std::make_pair(0, 0)) - a.begin();
        std::sort(a.begin() + 1, a.begin() + p, [](std::pair<int, int> x, std::pair<int, int> y) { return x.second == y.second ? x.first < y.first : x.second > y.second; });
        std::sort(a.begin() + p + 1, a.end(), [](std::pair<int, int> x, std::pair<int, int> y) { return x.second == y.second ? x.first < y.first : x.second < y.second; });
        std::vector<std::vector<long double> > f(n + 1, std::vector<long double> (n + 1, 1e18));
        f[1][n] = 0.;
        auto at = [&](int i, long double t) {
            return std::fabs(a[i].first + t * a[i].second * (i < p ? -1 : 1));
        };
        auto calc = [&](int i, long double t) {
            return at(i, t) / (v - a[i].second);
        };
        // for (int i = 1; i <= n; ++i)
        //     printf("%d: (%d, %d)\n", i, a[i].first, a[i].second);
        for (int i = 1; i <= p; ++i)
            for (int j = n; j >= p; --j) {
                if (i == p && j == p)
                    break;
                long double d = 0.;
                // printf("[%d, %d]: %lf\n", i, j, f[i][j]);
                for (int k = i; k < p; ++k) {
                    if (at(k, f[i][j]) >= at(i, f[i][j]) - eps)
                        d = std::max(d, calc(k, f[i][j]));
                    // printf("  k1 = %d, d = %lf\n", k, d);
                    f[k + 1][j] = std::min(f[k + 1][j], f[i][j] + d + (k != j - 1) * d);
                }
                d = 0.;
                for (int k = j; k > p; --k) {
                    if (at(k, f[i][j]) >= at(j, f[i][j]) - eps)
                        d = std::max(d, calc(k, f[i][j]));
                    // printf("  k2 = %d, d = %lf\n", k, d);
                    f[i][k - 1] = std::min(f[i][k - 1], f[i][j] + d + (k != i + 1) * d);
                }
            }
        {
            static int casetot = 0;
            std::cout << "Case #" << ++casetot << ": ";
        }
        std::cout << std::fixed << std::setprecision(9) << f[p][p] << '\n';
    }
    return 0;
}

一言 - Hitokoto