挑战赛 Round 87 题解

由于牛客的渲染问题,你可以点此链接进入我的博客查看

A MuQ 的签到题

知识点:位运算、数学、不等式证明、贪心/极值思维

设选出的符卡的最大公因数为 。因为这 张卡都必须是 的倍数,而 的倍数最多只有 个,所以 ,从而得到:

利用异或与加法的基本性质: ,再由 可得 ,结合 ,有:

所以答案最多是

  • 为偶数:直接选所有的数字,有 ,答案为

  • 为奇数且 :选前 (即 为偶数,选法同上),答案至少为 。接下来证明 不可能达到。

    ,由于前面已经有 ,所以必须每个不等号都取等号。由 可知, 只能是一对 。但此时, (当 为奇数),显然不是 ,矛盾。

    所以 无法达到,最大值只能是

  • 特判当 时,只能选择 ,答案为

时间复杂度

void solve() {
    int n;
    cin >> n;
    if (n == 1) cout << 0 << endl;
    else cout << ((n & 1) ? n : n + 1) << endl;
}

B CirnoNine 在梦里的画

知识点:二维前缀和、二维差分、图的连通性判定(BFS / DFS)、二分答案

一个更大的全黑正方形,总能被若干个更小的全黑正方形按路径方式拆出来,原来的合法路径也可以被“细化”成更小方块构成的路径。因此,可行性对 是单调的,最大可行值可以二分求出。

Q1:固定一个 ,怎样判断它是否可行?

如果一个 的正方形是全 ,那么称它的左上角为一个合法位置。对于固定的 ,我们先找出所有合法位置。设这些合法位置组成的集合为 。那么要让 可行,必须满足两件事:

  1. 里的所有位置必须处于同一个四连通块中;
  2. 所有合法位置对应的 方块并起来,必须恰好覆盖所有黑格(所有 1)。

为什么这样就够了?

  • 如果 连通,那么可以在这张“合法位置图”上做一次 ,构造出一条可以重复走点的路径,依次经过所有合法位置。
  • 每个合法位置对应的 方块都全是 ,而所有黑格又都被这些方块覆盖,那么这些方块的并集就正好是原矩阵中的所有

所以,固定 的判定就变成:判断“所有合法位置是否连通”以及“所有黑格是否都被某个合法位置对应的方块覆盖”。

Q2:如何高效判断固定 是否可行?

用二维前缀和判断一个 区域是否全 ,对于每个左上角 ,我们都想知道它对应的 区域里是不是全 。用二维前缀和可以做到 查询,若某个 区域内 的个数等于 ,则说明它是合法位置。

把每个合法位置看成图上的一个点,如果两个合法位置上下左右相邻,就连一条边。然后从任意一个合法位置开始 ,如果最终能访问到所有合法位置,说明它们连通,否则说明无法形成一条路径。

把每个合法位置对应的 方块都加进去。为了高效统计覆盖情况,使用二维差分,对每个合法位置 ,令它覆盖矩形 ,用二维差分把这些矩形全部加上,最后做一次二维前缀和,得到每个格子被覆盖了多少次。如果某个原矩阵里的 没有被覆盖到,则 不可行。

时间复杂度

void solve() {
    int n, m;
    cin >> n >> m;
    vector<string> g(n);
    for (int i = 0; i < n; ++i) cin >> g[i];

    vector<int> ps((n + 1) * (m + 1), 0);
    auto idx = [&](int x, int y) { return x * (m + 1) + y; };
    for (int i = 0; i < n; ++i) {
        int S = 0;
        for (int j = 0; j < m; ++j) {
            S += g[i][j] == '1';
            ps[idx(i + 1, j + 1)] = ps[idx(i, j + 1)] + S;
        }
    }

    auto calc = [&](int x1, int y1, int x2, int y2) { return ps[idx(x2, y2)] - ps[idx(x1, y2)] - ps[idx(x2, y1)] + ps[idx(x1, y1)]; };

    auto check = [&](int a) {
        int H = n - a + 1;
        int W = m - a + 1;
        if (H <= 0 || W <= 0) return false;

        vector<bool> flag(H * W, 0);
        vector<int> pos;

        for (int i = 0; i < H; ++i) {
            for (int j = 0; j < W; ++j) {
                if (calc(i, j, i + a, j + a) == a * a) {
                    flag[i * W + j] = 1;
                    pos.pb(i * W + j);
                }
            }
        }
        if (pos.empty()) return false;

        vector<bool> vis(H * W);
        queue<int> q;
        q.push(pos[0]);
        vis[pos[0]] = 1;
        int cnt = 1;

        while (!q.empty()) {
            int cur = q.front();
            q.pop();
            int x = cur / W, y = cur % W;

            auto add = [&](int nx, int ny) {
                int nxt = nx * W + ny;
                if (flag[nxt] && !vis[nxt]) {
                    vis[nxt] = 1;
                    ++cnt;
                    q.push(nxt);
                }
            };

            if (x > 0) add(x - 1, y);
            if (x + 1 < H) add(x + 1, y);
            if (y > 0) add(x, y - 1);
            if (y + 1 < W) add(x, y + 1);
        }

        if (cnt != pos.size()) return false;

        vector<int> diff((n + 1) * (m + 1), 0);

        for (auto v : pos) {
            int x = v / W, y = v % W;
            diff[idx(x, y)] += 1;
            diff[idx(x + a, y)] -= 1;
            diff[idx(x, y + a)] -= 1;
            diff[idx(x + a, y + a)] += 1;
        }

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                int cur = diff[idx(i, j)];
                if (i > 0) cur += diff[idx(i - 1, j)];
                if (j > 0) cur += diff[idx(i, j - 1)];
                if (i > 0 && j > 0) cur -= diff[idx(i - 1, j - 1)];
                diff[idx(i, j)] = cur;
            }
        }

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                if (g[i][j] == '1' && diff[idx(i, j)] <= 0) {
                    return false;
                }
            }
        }

        return true;
    };

    int lo = 1, hi = min(n, m), ans = 0;
    while (lo <= hi) {
        int mid = (lo + hi) >> 1;
        if (check(mid))
            ans = mid, lo = mid + 1;
        else
            hi = mid - 1;
    }

    cout << ans << endl;
}

C Papy 的数列求和

知识点:二进制字符串逐位 ​ ,数列求和拆项,模运算,构造递推式

要求计算:

把式子拆开:

因此问题本质上变成同时维护两个前缀和:

最终答案就是:

由于题目中的 是一个二进制整数,长度可达 ,不能直接转成普通整数,也不能用朴素循环枚举 。所以我们考虑按二进制逐位扩展转移。

设当前已经处理完某个二进制前缀,其对应的数值为

我们维护四个量:

  • :当前前缀代表的整数值(模 ),初始
  • ,初始
  • ,初始
  • ,初始

接下来计算转移

  • 如果读到的是 ,即

      • 其中
  • 如果读到的是 ,即

当二进制串全部处理完后,当前的 就是 ,此时:

所以答案为:

时间复杂度

void solve() {
    ll mod, a, b, q;
    cin >> mod >> a >> b >> q;
    Z::setMod(mod);
    string n;
    cin >> n;
    Z s1(0), s2(0), p(1), x(0);
    for (auto v : n) {
        if (v == '0') {
            s2 = s2 * (1 + p) + x * p * s1;
            s1 *= 1 + p;
            p *= p;
            x *= 2;
        } else {
            Z np = p * p * q;
            s2 = s2 * (1 + p) + x * p * s1 + (2 * x + 1) * np;
            s1 = s1 * (1 + p) + np;
            p = np;
            x = 2 * x + 1;
        }
    }
    cout << a * s2 + b * s1 << endl;
}

D Kendieer 的 LCP 问题

知识点:字典序排序,LCP,RMQ / Sparse Table,单调栈,贡献法 / 计数法,组合计数

先把所有字符串按字典序全局排序,记排序后的编号为 。设相邻两个字符串 长度为:

形成一个长度为 的数组。

对于任意两个在全局字典序中位置分别为 的字符串,有:

所以,查询里若选出的字符串在全局排序后的下标为 ,那么该子集的 长度就是:

于是问题变成:

  • 对每个查询给出的 个位置,排序后得到一个长度为 的序列;
  • 每个非空子集的贡献,等价于这组点对应的相邻间隔 的最小值;
  • 统计所有非空子集的最小值之和。

若子集大小为 ,则 就是该字符串本身长度。因此先把查询中所有字符串长度加起来:

若子集大小 ,设查询中排序后的字符串位置为 。定义:

全局字典序排序后,任意两串 等于它们在排序序列中间所有相邻 的最小值:

所以 可以通过全局 数组的 时间求得。

接下来只需要统计:所有非空子集里,最小间隔恰好由某个 贡献的子集数量。对每个间隔 (即 ),我们要找到它作为最小值的最大影响范围。

我们用单调栈求:

  • :使得在区间 内, 是严格最小的位置;
  • :使得在区间 内, 仍可作为最小值的最右范围。

左边维护严格单调递增栈,遇到 弹出,右边维护单调递增栈,遇到 弹出。这样处理相等值时,统一把贡献分配给最左边的那个最小值,避免重复统计。

作为一个子集 的主导最小值,那么这个子集必须:

  • 左侧至少选一个字符串;
  • 右侧至少选一个字符串;
  • 选中的字符串都必须落在 的有效区间内。

因此:

  • 左侧可选字符串个数为
  • 右侧可选字符串个数为

各自必须非空,故方案数为:

于是 的贡献就是:

把所有 的贡献加起来,再加上单点子集的字符串长度和,就是答案。

vector<Z> p2(1);
void init() {
    p2[0] = 1;
    for (int i = 1; i <= 500000; ++i) p2.pb(p2.back() * 2);
}

void solve() {
    int n, q;
    cin >> n >> q;
    vector<string> s(n + 1);
    for (int i = 1; i <= n; ++i) cin >> s[i];

    vector<int> ord(n);
    iota(all(ord), 1);
    sort(all(ord), [&](int a, int b) { return s[a] < s[b]; });
    vector<int> pos(n + 1);
    for (int i = 0; i < n; ++i) pos[ord[i]] = i;

    auto get = [&](string a, string b) {
        int len = min(a.size(), b.size());
        for (int i = 0; i < len; ++i) {
            if (a[i] != b[i]) return i;
        }
        return len;
    };
    vector<int> lcp(max(1, n - 1));
    for (int i = 0; i < n - 1; ++i) {
        lcp[i] = get(s[ord[i]], s[ord[i + 1]]);
    }

    int N = n <= 1 ? 1 : __lg(n - 1) + 1;
    vector<array<int, 18>> spt(n - 1);
    for (int i = 0; i < n - 1; ++i) spt[i][0] = lcp[i];

    for (int j = 1; j < N; ++j) {
        for (int i = 0; i + (1 << j) <= n - 1; ++i) {
            spt[i][j] = min(spt[i][j - 1], spt[i + (1 << (j - 1))][j - 1]);
        }
    }

    auto ask = [&](int L, int R) {
        int j = __lg(R - L + 1);
        return min(spt[L][j], spt[R - (1 << j) + 1][j]);
    };

    while (q--) {
        int k;
        cin >> k;
        vector<int> idx(k);
        for (int i = 0; i < k; ++i) cin >> idx[i];

        vector<int> ranks(k);
        for (int i = 0; i < k; ++i) ranks[i] = pos[idx[i]];
        sort(all(ranks));

        Z tot = 0;
        for (int i = 0; i < k; ++i) tot += s[ord[ranks[i]]].size();

        if (k == 1) {
            cout << tot << " ";
            continue;
        }

        vector<int> L(k);
        for (int i = 1; i < k; ++i) {
            L[i] = ask(ranks[i - 1], ranks[i] - 1);
        }

        vector<int> left(k), right(k), stk;

        for (int i = 1; i < k; ++i) {
            while (!stk.empty() && L[stk.back()] > L[i]) stk.pob();
            left[i] = stk.empty() ? 1 : stk.back() + 1;
            stk.pb(i);
        }

        stk.clear();
        for (int i = k - 1; i >= 1; --i) {
            while (!stk.empty() && L[stk.back()] >= L[i]) stk.pob();
            right[i] = stk.empty() ? k - 1 : stk.back() - 1;
            stk.pb(i);
        }

        for (int i = 1; i < k; ++i) tot += Z(L[i]) * (p2[i - left[i] + 1] - 1) * (p2[right[i] - i + 1] - 1);

        cout << tot << " ";
    }
    cout << endl;
}

E CirnoNine 找凸四边形

知识点:计算几何,叉积 / 方向判断,Andrew 凸包,凸包层剥离

先猜一个剥几层凸包下来,一定能在产生的点集中找到凸四边形,就有:

  • 对点集求凸包,如果凸包的点数 ,那么输出四个点即可。

  • 如果本层点数 ,那么我们把点加入最终暴力枚举的点集中。

  • 最终枚举四个点,检查这四个点是否能够组成凸四边形即可。

接下来我们证明如果存在凸四边形,在前三层中一定能够找到合法的一组答案:

设在所有凸四边形里,取一个使“四个顶点中最大层数”尽量小的,记为 ,并设 是其中最深的顶点。若 已经在前三层里,结论成立;下面假设 在第 层及以后。

因为第 层及以后一定在前三层点集 的凸包里,所以 。由二维 定理,存在 ,使得 .

现在固定四边形的另外三个点 。能作为第四个点、并且仍与 构成同向凸四边形的点,构成一个开凸区域 ;而

这里用一个交换引理:

如果一个点 在凸区域 内,同时 又在某个三角形 内,那么只要 是某个凸四边形的可替换顶点,就可以在 中找到一个点 。于是用 替换 后,仍然得到凸四边形,而且 来自前三层。

这样就得到一个“最深层数更小”的凸四边形,和我们取的最优性矛盾。

因此,假设不成立,说明最优凸四边形的四个顶点都能放到前三层里。结论成立。

template <class Int>
struct Point {
    Int x, y;
    int id;
    Point(const Int &x_ = 0, const Int &y_ = 0) : x(x_), y(y_) {}
    bool operator<(const Point &o) const {
        if (x != o.x) return x < o.x;
        return y < o.y;
    }
};

template <class Int>
Int cross(const Point<Int> &a, const Point<Int> &b, const Point<Int> &c) {
    return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}

template <class Int>
vector<Point<Int>> convex(vector<Point<Int>> pts) {
    int n = pts.size();
    if (n <= 2) return pts;

    sort(all(pts));

    vector<Point<Int>> hull;

    for (int i = 0; i < n; ++i) {
        while (hull.size() >= 2 && cross(hull[hull.size() - 2], hull.back(), pts[i]) <= 0) {
            hull.pop_back();
        }
        hull.push_back(pts[i]);
    }
    int ls = hull.size();
    for (int i = n - 2; i >= 0; --i) {
        while (hull.size() > ls && cross(hull[hull.size() - 2], hull.back(), pts[i]) <= 0) {
            hull.pop_back();
        }
        hull.push_back(pts[i]);
    }
    if (hull.size() > 1) hull.pop_back();
    return hull;
}

void solve() {
    int n;
    cin >> n;
    vector<Point<ll>> pts(n);
    for (int i = 0; i < n; i++) cin >> pts[i].x >> pts[i].y, pts[i].id = i + 1;

    sort(all(pts));

    vector<bool> vis(n + 1);
    vector<Point<ll>> cand;

    for (int i = 0; i < 3; ++i) {
        vector<Point<ll>> cur;
        for (auto v : pts) {
            if (!vis[v.id]) cur.pb(v);
        }

        if (cur.size() < 3) {
            for (auto v : cur) cand.pb(v);
            break;
        }

        auto hull = convex(cur);
        if (hull.size() >= 4) {
            cout << hull[0].id << " " << hull[1].id << " " << hull[2].id << " " << hull[3].id << "\n";
            return;
        }

        for (auto v : hull) {
            vis[v.id] = true;
            cand.pb(v);
        }
    }

    int m = cand.size();
    if (m >= 4) {
        sort(all(cand));
        for (int i = 0; i < m; ++i) {
            for (int j = i + 1; j < m; ++j) {
                for (int k = j + 1; k < m; ++k) {
                    for (int l = k + 1; l < m; ++l) {
                        vector<Point<ll>> sub = {cand[i], cand[j], cand[k], cand[l]};
                        vector<Point<ll>> shull = convex(sub);
                        if (shull.size() == 4) {
                            for (int it = 0; it < 4; ++it) {
                                cout << shull[it].id << " ";
                            }
                            cout << endl;
                            return;
                        }
                    }
                }
            }
        }
    }

    cout << -1 << endl;
}

F MuQ 的魔咒

知识点: 贪心,字典序,后缀数组 / 后缀排名比较,线段树 + 懒标记,区间维护与动态选择,字符串分段处理

要求从原串 中选出 恰好 个互不相交的非空子串,按原顺序拼接后,使得到的新字符串字典序最大。

本题的核心不是“选哪些区间”,而是:

  • 在字典序比较下,尽量让前面的字符更大
  • 如果前缀相同,就尽量让后面的部分更优
  • 由于区间不能重叠,因此一旦某段字符被纳入答案,它对后续可选位置会产生影响

因此可以把问题理解成一个“按字典序贪心选择若干连续块”的过程。

有如下观察:

  1. 字典序比较是“从前往后,先看更大的字符”,所以答案的构造一定是贪心的,即在当前还没处理的部分里,优先考虑字典序更大的字符。
  2. 当当前字符相同的时候,要比较的是后缀,比如两个候选区间都以 开头,那么谁更优,不是看 ,而是看,这段字符后面接什么,也就是比较它们的后缀字典序 -这就需要一个可以快速比较任意两个后缀大小的数据结构。
  3. 区间的“同字符连续段”可以整体处理 对于某个字符 c,在当前未处理部分中,它会形成若干个连续段。这些连续段是当前层面上的候选块,如果某个段左边已经接上了之前选过的位置,那它通常必须整段吸收,如果候选段太多,则需要按照后缀大小挑出最优的若干段

我们对原串建立后缀数组,得到每个位置 的后缀排名 。这样就能在 时间内比较两个后缀, ,说明 的字典序更大。所以比较时如果两个候选段首字符相同,就直接比较它们的后缀排名,决定保留哪个。

按字符从 扫描。对当前字符

  1. 找出当前尚未选过的所有 的连续段
  2. 如果某段左侧紧挨着已经选过的位置,那么这段通常要直接并入答案
  3. 若当前段数超过剩余可用配额,则按后缀排名排序,保留更优的段
  4. 对于最终保留下来的段,直接把其中字符全部加入答案

直观理解就是:

  • 先尽量让答案前面出现更大的字母
  • 同字母时,优先保留后缀更大的段
  • 不能保留的段,就尽量完整地吸收掉,避免浪费位置

对于维护和查询后缀排名,我们使用线段树。每个位置的叶子节点存当前字符大小和当前位置编号,比较规则即为:先比字符大小,字符相同再比位置。

当位置 被选入答案时:

  • 标记为已选
  • 修改区间 的优先级,让后续查询更倾向于从右侧继续选取合适字符

这样可以在动态删除/选择位置的同时,仍然快速找到当前最优的剩余位置。

时间复杂度:

struct SegTree {
    struct Node {
        int l = 0, r = 0;
        array<ll, 3> val{0, 0, -1};
        ll lazy = 0;
    };
 
    int n = 0;
    vector<Node> tr;
 
    static array<ll, 3> mergeVal(const array<ll, 3>& a, const array<ll, 3>& b) {
        if (a[2] == -1) return b;
        if (b[2] == -1) return a;
        return min(a, b);
    }
 
    void pull(int u) { tr[u].val = mergeVal(tr[u << 1].val, tr[u << 1 | 1].val); }
 
    void apply(int u, ll add) {
        tr[u].lazy += add;
        tr[u].val[0] += add;
    }
 
    void push(int u) {
        if (!tr[u].lazy) return;
        apply(u << 1, tr[u].lazy);
        apply(u << 1 | 1, tr[u].lazy);
        tr[u].lazy = 0;
    }
 
    void build(int u, int l, int r, const vector<int>& a) {
        tr[u].l = l;
        tr[u].r = r;
        tr[u].lazy = 0;
        if (l == r) {
            tr[u].val = {0, -(ll)a[l], l};
            return;
        }
        int m = (l + r) >> 1;
        build(u << 1, l, m, a);
        build(u << 1 | 1, m + 1, r, a);
        pull(u);
    }
 
    void build(const vector<int>& a) {
        n = (int)a.size() - 1;
        tr.assign(4 * n + 5, {});
        build(1, 1, n, a);
    }
 
    void update(int u, int l, int r, int ql, int qr, ll add) {
        if (ql > r || qr < l) return;
        if (ql <= l && r <= qr) {
            apply(u, add);
            return;
        }
        push(u);
        int m = (l + r) >> 1;
        update(u << 1, l, m, ql, qr, add);
        update(u << 1 | 1, m + 1, r, ql, qr, add);
        pull(u);
    }
 
    void update(int l, int r, ll add) { update(1, 1, n, l, r, add); }
 
    array<ll, 3> query(int u, int l, int r, int ql, int qr) {
        if (ql > r || qr < l) return {0, 0, -1};
        if (ql <= l && r <= qr) return tr[u].val;
        push(u);
        int m = (l + r) >> 1;
        return mergeVal(query(u << 1, l, m, ql, qr), query(u << 1 | 1, m + 1, r, ql, qr));
    }
 
    array<ll, 3> query(int l, int r) { return query(1, 1, n, l, r); }
};
 
struct SuffixArray {
    int n = 0;
    vector<int> sa, rk, tmp, cnt;
 
    void build(const string& s) {
        n = (int)s.size() - 1;
        sa.assign(n + 1, 0);
        rk.assign(n + 1, 0);
        tmp.assign(n + 1, 0);
        cnt.assign(max(256, n) + 5, 0);
 
        int m = 256;
        fill(cnt.begin(), cnt.begin() + m, 0);
 
        for (int i = 1; i <= n; ++i) cnt[rk[i] = (unsigned char)s[i]]++;
        for (int i = 1; i < m; ++i) cnt[i] += cnt[i - 1];
        for (int i = n; i >= 1; --i) sa[cnt[rk[i]]--] = i;
 
        for (int len = 1, p = 0; p < n; len <<= 1) {
            p = 0;
            for (int i = max(1, n - len + 1); i <= n; ++i) tmp[++p] = i;
            for (int i = 1; i <= n; ++i)
                if (sa[i] > len) tmp[++p] = sa[i] - len;
 
            fill(cnt.begin(), cnt.begin() + m, 0);
            for (int i = 1; i <= n; ++i) cnt[rk[tmp[i]]]++;
            for (int i = 1; i < m; ++i) cnt[i] += cnt[i - 1];
            for (int i = n; i >= 1; --i) sa[cnt[rk[tmp[i]]]--] = tmp[i];
 
            tmp[sa[1]] = 1;
            p = 1;
            for (int i = 2; i <= n; ++i) {
                int a = sa[i], b = sa[i - 1];
                int ra = rk[a], rb = rk[b];
                int r1 = (a + len <= n ? rk[a + len] : 0);
                int r2 = (b + len <= n ? rk[b + len] : 0);
                if (ra == rb && r1 == r2)
                    tmp[a] = p;
                else
                    tmp[a] = ++p;
            }
            for (int i = 1; i <= n; ++i) rk[i] = tmp[i];
            m = p + 1;
            if (p == n) break;
        }
    }
};

void solve() {
    int n, k;
    cin >> n >> k;
    string s;
    cin >> s;
    s = ' ' + s;
 
    SuffixArray SA;
    SA.build(s);
 
    vector<int> rankv(n + 1);
    for (int i = 1; i <= n; ++i) rankv[i] = SA.rk[i];
 
    vector<int> a(n + 1);
    for (int i = 1; i <= n; ++i) a[i] = s[i];
 
    SegTree tr;
    tr.build(a);
 
    auto betterSuffix = [&](int x, int y) { return rankv[x] > rankv[y]; };
 
    vector<char> vis(n + 1, 0);
    vector<int> lenCnt(n + 1, 0);
    int bucketCnt = 0;
    int now = 1, cnt = 0, remain = k;
 
    auto sel = [&](int x) {
        tr.update(x, n, -1);
        tr.update(x, x, (ll)n * 10);
        vis[x] = 1;
        ++cnt;
    };
 
    for (char c = 'z'; c >= 'a' && now <= n; --c) {
        vector<pii> seg;
        for (int i = now; i <= n; ++i) {
            if (vis[i] || s[i] != c) continue;
            if (i == now || s[i - 1] != c)
                seg.push_back({i, i});
            else
                seg.back()[1] = i;
        }
 
        if (seg.empty()) continue;
 
        if (vis[seg[0][0] - 1]) {
            for (int i = seg[0][0]; i <= seg[0][1]; ++i) sel(i);
            seg.erase(seg.begin());
        }
 
        if ((int)seg.size() > remain) {
            auto ord = seg;
            sort(ord.begin(), ord.end(), [&](const pii& A, const pii& B) { return betterSuffix(A[0], B[0]); });
 
            int tmpLen = 0;
            for (int i = 0; i < remain; ++i) {
                auto [l, r] = ord[i];
                if (lenCnt[r - l]++ == 0) ++bucketCnt;
                tmpLen = r - l;
            }
 
            vector<pii> keep;
            int j = 0;
            for (; j < (int)seg.size(); ++j) {
                auto [l, r] = seg[j];
                if (lenCnt[r - l] == 0) continue;
                if (--lenCnt[r - l] == 0) --bucketCnt;
 
                if (r - l == tmpLen) {
                    keep.push_back({l, r});
                } else {
                    for (int i = l; i <= r; ++i) {
                        sel(i);
                        now = max(now, i + 1);
                    }
                    --remain;
                }
                if (bucketCnt == 0) break;
            }
 
            seg[j][0] = seg[j][1] - tmpLen;
            pii best = seg[j];
            for (int i = j + 1; i < (int)seg.size(); ++i) {
                if (betterSuffix(seg[i][0], best[0])) best = seg[i];
            }
 
            if (best != seg[j]) {
                keep.pop_back();
                keep.push_back(best);
            }
 
            for (auto [l, r] : keep) {
                for (int i = l; i <= r; ++i) {
                    sel(i);
                    now = max(now, i + 1);
                }
                --remain;
            }
 
            while (now <= n) sel(now++);
        } else {
            for (auto [l, r] : seg) {
                for (int i = l; i <= r; ++i) {
                    sel(i);
                    now = max(now, i + 1);
                }
                --remain;
            }
        }
    }
 
    while (cnt < k) {
        int id = tr.query(1, n)[2];
        sel(id);
    }
 
    for (int i = 1; i <= n; ++i)
        if (vis[i]) cout << s[i];
    cout << endl;
}

G 矩阵的秩

知识点:有限域上的线性代数, 模拟与高斯二项式系数, 反演,多项式分治卷积, 变换(等比数列多点求值),多项式卷积

在有限域 中,长度为 的非零向量共有 个。两个向量 成比例( )当且仅当它们属于同一个一维子空间。在 中,共有 个不同的一维子空间。

矩阵的 个行向量互不成比例,意味着这 个行向量必须属于 个不同的一维子空间。

设我们在射影空间 中选择了 个不同的点(每个点对应一个一维子空间)。我们需要计算这 个点生成的张成空间的维数为 的方案数。

为在射影空间中选择 个不同点且张成空间维数为 的有序序列数。根据 反演公式,总方案数为:

其中:

  • 是高斯二项式系数。

  • 维向量空间中一维子空间的数量。

  • 是下降阶乘幂,也就是排列数。

。设 。我们可以用分治 + 内求出该多项式的系数。

利用 变换,我们可以在 内求出所有

最后通过一次卷积即可求出所有的

template <ll G>
struct Poly {
    static void ntt(vector<Z> &a, bool invert) {
        int n = a.size();
        vector<int> rev(n);
        int lg = __builtin_ctz(n);
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < lg; j++)
                if (i & (1 << j)) rev[i] |= 1 << (lg - 1 - j);
            if (i < rev[i]) swap(a[i], a[rev[i]]);
        }
 
        for (int len = 2; len <= n; len <<= 1) {
            Z wlen = ksm(Z(G), (MOD - 1) / len);
            if (invert) wlen = Z(1) / wlen;
            for (int i = 0; i < n; i += len) {
                Z w = 1;
                for (int j = 0; j < len / 2; j++) {
                    Z u = a[i + j], v = a[i + j + len / 2] * w;
                    a[i + j] = u + v;
                    a[i + j + len / 2] = u - v;
                    w *= wlen;
                }
            }
        }
 
        if (invert) {
            for (auto &x : a) x /= n;
        }
    }
    static vector<Z> multiply(vector<Z> a, vector<Z> b) {
        int res_deg = a.size() + b.size() - 2;
        int sz = 1;
        while (sz < a.size() + b.size()) sz <<= 1;
        a.resize(sz);
        b.resize(sz);
        ntt(a, false);
        ntt(b, false);
        for (int i = 0; i < sz; i++) a[i] *= b[i];
        ntt(a, true);
        a.resize(res_deg + 1);
        return a;
    }
};
 
using NTT = Poly<3>;

void solve() {
    int n, m, p;
    cin >> n >> m >> p;
 
    int K = min(n, m), N = max(n, m);
 
    vector<Z> pow(N + 1);
    pow[0] = 1;
    for (int i = 1; i <= N; ++i) pow[i] = pow[i - 1] * p;
 
    vector<Z> fac(N + 1), invfac(N + 1);
    fac[0] = 1;
    for (int i = 1; i <= N; ++i) fac[i] = fac[i - 1] * (pow[i] - 1);
    invfac[N] = Z(1) / fac[N];
    for (int i = N - 1; i >= 0; --i) invfac[i] = invfac[i + 1] * (pow[i + 1] - 1);
 
    vector<Z> d(n);
    for (int i = 0; i < n; ++i) d[i] = Z(i) * (p - 1) + 1;
    auto build = [&](auto &&self, int L, int R) -> vector<Z> {
        if (L == R) return {-d[L], 1};
        int mid = (L + R) / 2;
        return NTT::multiply(self(self, L, mid), self(self, mid + 1, R));
    };
 
    vector<Z> coeffs = build(build, 0, n - 1);
 
    int tot = n + K;
    vector<Z> pw(tot + 1);
    pw[0] = 1;
    Z pp = 1;
    for (int i = 1; i <= tot; ++i) pw[i] = pw[i - 1] * pp, pp *= p;
 
    vector<Z> xr(n + 1), yt(tot + 1);
    for (int a = 0; a <= n; ++a) xr[n - a] = coeffs[a] / pw[a];
    for (int i = 0; i <= tot; ++i) yt[i] = pw[i];
 
    vector<Z> ce = NTT::multiply(xr, yt), pj(K + 1);
    for (int j = 0; j <= K; ++j) pj[j] = ce[n + j] / pw[j];
 
    vector<Z> A(K + 1), B(K + 1);
    for (int i = 0; i <= K; ++i) A[i] = ((i & 1) ? -1 : 1) * (pw[i] * invfac[i]), B[i] = pj[i] * invfac[i];
 
    vector<Z> fin = NTT::multiply(A, B);
    for (int k = 0; k <= K; ++k) cout << fin[k] * fac[m] * invfac[m - k] << " ";
    cout << endl;
}