构造类问题的很少解题方法
2025-05-18

病毒可能具有膜结构,但不存在生物膜系统。核糖体是唯一所有细胞均含有的细胞器,但病毒中无核糖体。

病毒的主要组成是 \(10\%\sim 20\%\) 的核酸,\(60\%\sim 70\%\) 的蛋白质外壳,\(<10\%\) 的结合水,可能具有逆转录酶、RNA 聚合酶。病毒的含水量(\(<10\%\))远远小于细胞(\(70\%\))。


Type I:调整法 - 1

虽然话是这么说,感觉这就是平常正常的做题路径,『想做法』——『发现有锅』——『打补丁』。

只是可能这是在提醒你在构造题中发现有锅不要急着换做法(?)


例题:C - Stations

https://qoj.ac/problem/1139

一个简单的想法是,当可用的编号范围很大时,可以记下每个点 \(u\)\(DFN_u\) 和出栈序(记为 \(RFN_u\)),这样就能解决查询;但标号是 \(N^2\) 级别的。

现在思考,我们为什么需要记录 \(RFN_u\) 呢?因为在询问时,需要判断 \(t\) 的位置:如果在 \(x\) 某一儿子的子树内,答案为该儿子;否则,答案为 \(fa\)。当 \(DFN_t\)\(u\) 最靠后的儿子 \(v\)\(DFN\) 还要大时,无法判断 \(t\)\(v\) 内还是在 \(u\) 外。

此处有一个解决方案(原谅我实在无法猜出是怎么想到的),将树按奇数层、偶数层分层,计数层记录 \(DFN\),偶数层记录 \(RFN\)(具体地,奇数层在入栈时编号,偶数层在出栈时编号);接下来进行判断(注意我们并不知道 \(u\) 所在层数的奇偶性):

  • 若不存在 \(id_i>id_u\),说明 \(id_u\)\(RFN_u\);此时 可以判断 \(t\) 是否位于 \(u\) 内。
  • 否则,\(id_u\)\(DFN_u\)。由于知道 \(RFN_v\),可以判断 \(t\) 是否位于 \(v\) 内。

容易证明其他一般情况也可以判断 \(t\) 的位置。复杂度 \(O(n^2)\)

#include "stations.h"
#include <bits/stdc++.h>
std::vector<int> label(int n, int k, std::vector<int> u, std::vector<int> v) {
    std::vector<std::vector<int> > g(n);
    for (int i = 0; i < n - 1; ++i)
        g[u[i]].push_back(v[i]), g[v[i]].push_back(u[i]);
    std::vector<int> id(n, -1);
    int now = 0;
    std::function<void(int, int, int)> DFS = [&](int x, int fa, int tag) {
        if (tag)
            id[x] = now++;
        for (auto i : g[x])
            if (i != fa)
                DFS(i, x, tag ^ 1);
        if (!tag)
            id[x] = now++;
        return;
    };
    DFS(0, -1, 1);
    return id;
}
int find_next_station(int s, int t, std::vector<int> c) {
    if (c.back() < s) {
        int fa = c.front();
        if (t > s)
            return fa;
        for (int i = (int)c.size() - 1; ~i; --i)
            if (t >= c[i])
                return c[i];
        return fa;
    }
    else {
        int fa = c.back();
        if (t < s)
            return fa;
        for (int i = 0; i < (int)c.size() - 1; ++i)
            if (t <= c[i])
                return c[i];
        return fa;
    }
    // assert(0);
    return 114514;
}

Type II:调整法 - 2

题目要求构造『恰好为 \(k\)』,可以先不看这个限制,对于局面求出上界和下界,然后再看是不是上下界中全部(或大多数)都能取到,此时有两个路径:

  • 直接在某个上界 / 下界局面中通过若干步极小改动调整到恰好为 \(k\)
  • 通过这一点优化 DP 状态(这样就可以大量压缩『可到达局面』这一信息)。见

例题:D - Construct the Binary Tree

https://codeforces.com/problemset/problem/1311/E

首先从找上下界的角度出发,发现链为上界,完全二叉树为下界。

那么只需先 check \(d\) 是否在该范围内;固定树最左侧的一条链,每次拿走右下角的一个叶子(这样就能维持完全二叉树性质),如果可以插入到链底就 do so;否则由于这是个左边挂着单链的完全二叉树,可以证明你想取的任意深度都可以取到,暴力跳即可,且跳完后就构造完了。

复杂度 \(O(n)\)

\(O(nd)\) 是每次取点时扫一遍完全二叉树找一个能让当前点深度 \(+1\) 的父节点。\(O(d)\) 的做法是慢慢把树变窄变高,一次还是只 \(+1\),二者的弊端都在于没利用『上界为链』即链和完全二叉树的优美性质。

#include <bits/stdc++.h>
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 n, d;
        std::cin >> n >> d;
        std::vector<int> tag(n + 1), dep(n + 1), cnt(n + 1), fa(n + 1);
        int L = 0, R = n * (n - 1) / 2;
        for (int i = 1; i <= n; ++i) {
            L += std::__lg(i), dep[i] = std::__lg(i);
            if (i * 2 <= n)
                ++cnt[i], fa[i * 2] = i;
            if (i * 2 + 1 <= n)
                ++cnt[i], fa[i * 2 + 1] = i;
        }
        if (L <= d && d <= R) {
            std::cout << "YES\n";
            int t = 1;
            for (int i = 1; i <= n; i *= 2)
                tag[i] = 1, t = i;
            for (int i = n; i && L != d; --i)
                if (!tag[i]) {
                    // printf("i = %d\n", i);
                    if (L + (dep[t] + 1) - dep[i] <= d) {
                        // printf("L += %d - %d\n", dep[t] + 1, dep[i]);
                        L += (dep[t] + 1) - dep[i];
                        --cnt[fa[i]], cnt[i] = 0, ++cnt[t];
                        dep[i] = dep[t] + 1, fa[i] = t;
                        t = i, tag[i] = 1;
                    }
                    else {
                        for (int j = 1; j <= n; ++j)
                            if (cnt[j] != 2 && L + (dep[j] + 1) - dep[i] == d) {
                                fa[i] = j, L = d;
                                break;
                            }
                    }
                }
            for (int i = 2; i <= n; ++i)
                std::cout << fa[i] << ' ';
            std::cout << '\n';
        }
        else {
            // printf("[%d, %d]\n", L, R);
            std::cout << "NO\n";
        }
    }
    return 0;
}

Type III:增量法 / 规约法

增量法:类似数归,发现可以方便地从 \(n-k\) 扩展到 \(n\),考虑 \(n-k\)\(k\) 带来的限制 / 性质,就可以类递推地做了。

规约法:发现抠掉一个好处理的 \(k\) 之后可以转化为规模为 \(n-k\) 的子问题,考虑 \(k\)\(n - k\) 带来的限制,也可以类递推地做。

其实真差不多哈,并不能说是一正一反之类的,因为思维路径真没太差。


例题:经典题

给定大小为 \(n\) 的竞赛图,\(O(n^2)\) 内求出一条哈密顿路径。

  • 竞赛图:给完全图的每条边定向。
  • 哈密顿路径:经过每个点恰好一次,对边无要求。

假设已经知道规模为 \(n-1\) 的子问题的解法,塞一个新点进去,考察 \(P(n-1)\) 中的 \(\forall\, u\to v\)

  • 若只存在 \(n\to u,n\to v\):对于路径起点 \(s\) 也有 \(n\to s\),把 \(n\) 添加到开头即可。
  • 若只存在 \(u\to n,v\to n\):对于路径终点 \(t\) 也有 \(t\to n\),把 \(n\) 添加到末尾即可。
  • 若只存在 \(n\to u,v\to n\):对于路径起点 \(s\) 也有 \(n\to s\),对于路径终点 \(t\) 也有 \(t\to n\),爱加哪儿就加哪儿。
  • 否则:存在 \(u\to n,n\to v\),皆大欢喜,将 \(u\to v\) 改为 \(u\to n\to v\) 即可。

由此就可以解决问题。


例题:E - Travelling Salesperson

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

注意本题为无向边!

相似地,对于 \(P(n-1)\),假如存在 \(u\to v\),欲加入 \(u\to n\to v\) 讨论以下几种情况:

  • \(P(n - 1)\) 中只含有一种颜色的边:直接加入首 / 尾即可。
  • 若存在 \(\color{red}{\to} u\color{red}{\to}v\color{red}{\to}\)\(u\color{red}{\to} n\)\(n\color{red}{\to} v\)(蓝色同理):直接加入,皆大欢喜。
  • 其余情况,就是 \(\color{red}{\to} u\color{blue}{\to} v\color{blue}{\to}\) 的情况了。容易发现除了 \(u\color{blue}{\to} n\land n\color{red}{\to} v\) 之外的情况都可以直接将边加入。故接下来讨论该特例。

    此时在 \((u,v)\) 处无法加入;尝试考虑相邻的点。由于在 \(u\color{blue}{\to} v\) 处切换颜色,易知 \(u\ne s\),即 \(u\) 存在前驱(记为 \(p\))。

    • 若存在 \(p\color{blue}{\to} i\):连接 \(p,i,u\),最终局面为 \(\color{red}{\to} p\color{blue}{\to} i\color{blue}{\to} u\color{blue}{\to} v\color{blue}{\to}\),即将变换处提前两位。
    • 否则:存在 \(p\color{red}{\to} i\),仍然连接 \(p,i,u\),最终局面为 \(\color{red}{\to} p\color{red}{\to} i\color{blue}{\to} u\color{blue}{\to} v\color{blue}{\to}\),即将变换处提前一位。

由此可解决问题。可以发现并不存在所谓无解的情况 —— 倒不如说可以对所有点套用最后一种情况(和第一种)——就能够 \(O(n^2)\) 解决原问题了。

loj 上过了但洛谷过不了

#include <bits/stdc++.h>
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 n;
    std::cin >> n;
    if (n == 1) {
        std::cout << "1\n1" << '\n';
        return 0;
    }
    std::vector<std::vector<char> > g(n + 1, std::vector<char> (n + 1));
    for (int i = 2; i <= n; ++i)
        for (int j = 1; j < i; ++j)
            std::cin >> g[i][j], g[j][i] = g[i][j];
    for (int i = 1; i <= n; ++i) {
        std::vector<int> tag(n + 1);
        std::list<int> p({ i, i == 1 ? 2 : 1 });
        tag[p.front()] = tag[p.back()] = 1;
        bool flag = 1;
        char R = g[p.front()][p.back()], B = ((R == 'R') ? 'B' : 'R');
        auto pos = --p.end();
        for (int j = 1; j <= n; ++j)
            if (!tag[j]) {
                if (flag && g[j][p.back()] == R)
                    // printf("%d: 30  ", j),
                    p.push_back(j), ++pos;
                else if (g[j][p.back()] == B)
                    // printf("%d: 33  ", j),
                    p.push_back(j), flag = 0;
                else {
                    auto u = pos, v = std::next(pos);
                    if (g[*u][j] == R && g[j][*v] == R) {
                        // printf("%d: 38  ", j),
                        p.insert(v, j), ++++pos;
                        if (v == --p.end())
                            flag = 1;
                    }
                    else if (g[*u][j] == R && g[j][*v] == B)
                        // printf("%d: 41  ", j),
                        p.insert(v, j), ++pos;
                    else if (g[*u][j] == B && g[j][*v] == B)
                        // printf("%d: 44  ", j),
                        p.insert(v, j);
                    else {
                        auto pr(std::prev(u));
                        if (g[*pr][j] == B)
                            // printf("%d: 49  ", j),
                            p.insert(u, j), ----pos;
                        else
                            // printf("%d: 52  ", j),
                            p.insert(u, j), --pos;
                    }
                }
                // for (auto j : p)
                //     std::cout << j << ' ';
                // printf(" flag = %d\n", flag);
            }
        std::cout << n << '\n';
        for (auto j : p)
            std::cout << j << ' ';
        std::cout << '\n';
    }
    return 0;
}

F - Sergey’s problem

https://codeforces.com/problemset/problem/1019/C


言论