挑战赛 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)。
为什么这样就够了?
- 如果
连通,那么可以在这张“合法位置图”上做一次
,构造出一条可以重复走点的路径,依次经过所有合法位置。
- 每个合法位置对应的
方块都全是
,而所有黑格又都被这些方块覆盖,那么这些方块的并集就正好是原矩阵中的所有
。
所以,固定 的判定就变成:判断“所有合法位置是否连通”以及“所有黑格是否都被某个合法位置对应的方块覆盖”。
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 的魔咒
知识点: 贪心,字典序,后缀数组 / 后缀排名比较,线段树 + 懒标记,区间维护与动态选择,字符串分段处理
要求从原串 中选出 恰好
个互不相交的非空子串,按原顺序拼接后,使得到的新字符串字典序最大。
本题的核心不是“选哪些区间”,而是:
- 在字典序比较下,尽量让前面的字符更大
- 如果前缀相同,就尽量让后面的部分更优
- 由于区间不能重叠,因此一旦某段字符被纳入答案,它对后续可选位置会产生影响
因此可以把问题理解成一个“按字典序贪心选择若干连续块”的过程。
有如下观察:
- 字典序比较是“从前往后,先看更大的字符”,所以答案的构造一定是贪心的,即在当前还没处理的部分里,优先考虑字典序更大的字符。
- 当当前字符相同的时候,要比较的是后缀,比如两个候选区间都以
开头,那么谁更优,不是看
,而是看,这段字符后面接什么,也就是比较它们的后缀字典序 -这就需要一个可以快速比较任意两个后缀大小的数据结构。
- 区间的“同字符连续段”可以整体处理
对于某个字符
c,在当前未处理部分中,它会形成若干个连续段。这些连续段是当前层面上的候选块,如果某个段左边已经接上了之前选过的位置,那它通常必须整段吸收,如果候选段太多,则需要按照后缀大小挑出最优的若干段
我们对原串建立后缀数组,得到每个位置 的后缀排名
。这样就能在
时间内比较两个后缀,
,说明
的字典序更大。所以比较时如果两个候选段首字符相同,就直接比较它们的后缀排名,决定保留哪个。
按字符从 到
扫描。对当前字符
:
- 找出当前尚未选过的所有
的连续段
- 如果某段左侧紧挨着已经选过的位置,那么这段通常要直接并入答案
- 若当前段数超过剩余可用配额,则按后缀排名排序,保留更优的段
- 对于最终保留下来的段,直接把其中字符全部加入答案
直观理解就是:
- 先尽量让答案前面出现更大的字母
- 同字母时,优先保留后缀更大的段
- 不能保留的段,就尽量完整地吸收掉,避免浪费位置
对于维护和查询后缀排名,我们使用线段树。每个位置的叶子节点存当前字符大小和当前位置编号,比较规则即为:先比字符大小,字符相同再比位置。
当位置 被选入答案时:
- 将
标记为已选
- 修改区间
的优先级,让后续查询更倾向于从右侧继续选取合适字符
这样可以在动态删除/选择位置的同时,仍然快速找到当前最优的剩余位置。
时间复杂度:
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;
}

京公网安备 11010502036488号