2026 暑假梦熊青岛集训 B1 Part 2
不懂电脑还是博客园被干冒烟了,开两篇。
2026.7.31 省选模拟赛 2
两年前不会,现在该会了。30min 切了。
大胆猜测答案可以达到上界 \(\frac{n(n-1)}{p(p-1)}\),手模一下感觉确实可以。
但是观察不出什么特别优美的构造,给 \(p^k\) 一定有道理啊,猜测是分治。
设当前对于 \(n=p^{k-1}\) 构造到了上界,要合并 \(p\) 堆东西。\(p\) 是质数让人想到剩余系相关内容,于是从第一堆里面每个个出发,一次以 \(0\sim len\) 为步长在每堆中选一个组成一组,是正确的。
#include <bits/stdc++.h>
using namespace std;
int p, k;
inline void solve(int st, int n) {
if (n == 1) return; int len = n / p;
for (int i = 0; i < n; i += len) solve(i + st, len);
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
for (int _ = 0, k = i; _ < p; _++)
cout << k + st + _ * len << ' ', k = (k + j) % len;
cout << '\n';
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n = 1; cin >> p >> k;
for (int i = 1; i <= k; i++) n *= p;
cout << "YES\n"; solve(0, n);
return 0;
}
两年前订正了,现在不会。关键在于不会普及组 dp。
诈骗的是,一个 \(b\) 对应唯一的一个 \(p\),所以相当于就是计数合法 \(b\) 的数量。这里有一个 \(O(n^3)\) 的区间 dp 做法,但没有什么前途。
上面那个做法对 \(b\) 的限制描述得太复杂了,发现实际上若 \(p\) 的一个数要从 \(x\) 移到 \(y(y>x)\),设 \(c_x\) 表示 \(b_i=x\) 的位置 \(i\),那么限制就是 \(c_x>c_{x+1}>\cdots c_y,c_y<c_{y+1}\),\(y<x\) 的时候同理。
限制之间矛盾了就不合法,否则对 \(b\) 的限制仅限于 \(i,i+1\) 之间的大小关系,就是一个有向链拓扑序计数问题。好笑吗,赛时想到这个了根本不会做。
设 \(f_{i,j}\) 表示前 \(i\) 个数,\(c_i\) 排名为 \(j\) 的方案数,转移到 \(i+1\) 值域由 \([1,i]\) 扩展到 \([1,i+1]\),是经典排列计数 dp。前缀和优化一下即可做到 \(O(n^2)\)。
代码懒得补了,直接贺以前写的,但似乎要去掉 #define int long long 不然空间炸了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int Mod = 998244353;
const int N = 5005;
int f[N][N], s[N][N], a[N];
signed main() {
freopen("permutation.in", "r", stdin);
freopen("permutation.out", "w", stdout);
int n; cin >> n;
for (int i = 1, x; i <= n; i++) {
cin >> x; if (!x) continue;
if (x > i) {
a[i] = a[x] = 1;
for (int j = i + 1; j < x; j++) a[j] = -1;
} else {
a[i] = a[x] = -1;
for (int j = x + 1; j < i; j++) a[j] = 1;
}
}
f[1][1] = 1; s[1][1] = 1;
for (int i = 2; i < n; i++)
for (int j = 1; j <= i; j++) {
if (!a[i]) f[i][j] = s[i - 1][i - 1];
else if (a[i] == -1) f[i][j] = s[i - 1][j - 1];
else f[i][j] = (s[i - 1][i - 1] - s[i - 1][j - 1] + Mod) % Mod;
s[i][j] = (s[i][j - 1] + f[i][j]) % Mod;
}
cout << s[n - 1][n - 1] << '\n';
return 0;
}
P12559 [UOI 2024] Zeroing the segment
刻画答案形式后的转化和处理都不好想,很精妙。
答案形式怎么推的懒得写了,设 \(t=\max\lfloor \log_2(a_i)\rfloor\),显然可以枚举一下 \(t\) 然后将 \(a_i<2^t\) 的扔掉,设扔掉后需要处理的集合为 \(S\),则答案为:
令 \(c_x=x-\sum\limits_{i\in S}[a_i\le x+2^t]\)。这里可以直接 Hall 定理转化,但也可以硬瞪出来,实际上是一个类似构造双射的东西:贡献是 \(|S|+\max_{x\ge 0} -c_x\),而 \(c_x\le 0\) 的部分一定由一些 \(i\) 来贡献,相当于必须扔掉这么多个的 \(i\),至少要扔掉这么多是显然的,而这题的形式比较特殊,可以证明一定存在某种扔掉的方式达到下界。那么就转化成:最多保留 \(S\) 中多少个数使得 \(\forall c_x,c_x\ge 0\)。
要研究清楚为什么要这么转化。因为如果保留上面的形式,在尝试扫描线的过程中就会发现信息不满足可减性而无法动态维护出每个左端点的答案。但是经过这个转化之后变成了一个类似 CF1100F 的贪心:只要我们在保留的过程中尽量保留靠往边的数,即尽量删右边的数,那么右端点固定的情况下 \(l\) 的答案就是查 \([l,r]\) 这一段保留数的个数(正确性琢磨一下应该比较显然),即动机是扫描线的可行性,而类似问题中可以尝试转化为贪心进而来做。
然后就比较简单了,维护保留的数可以用线段树来做,然后强制在线的话只要用主席树把扫描线的过程存下来就行了,要支持区间加单点查状物,写个标记永久化就行了。
虽然外面还有一层 \(t\),但注意到每个数和询问只会在一个 \(t\) 处被处理到,因此总的复杂度还是 \(O((n+q)\log n)\) 的。这题还有个主席树,因此空间也是 \(O(n\log n)\) 的。
#include <bits/stdc++.h>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
#define int long long
using namespace std;
const int N = 2e5 + 5, M = 60;
const int INF = 1e9;
int n, a[N], f[N][M], b[N], rk[N];
vector <int> p[M];
struct Segment {
int t[N << 2], mn[N << 2], tg[N << 2];
inline void pushup(int k) {
t[k] = min(t[ls(k)], t[rs(k)]);
mn[k] = min(mn[ls(k)], mn[rs(k)]);
}
inline void f(int k, int v) { t[k] += v, tg[k] += v; }
inline void pushdown(int k) {
f(ls(k), tg[k]), f(rs(k), tg[k]), tg[k] = 0;
}
void build(int k, int l, int r) {
tg[k] = 0; int m = l + r >> 1;
if (l == r)
return t[k] = a[b[l]], mn[k] = INF, void();
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k);
}
void updata(int k, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) return f(k, v);
int m = l + r >> 1; pushdown(k);
if (L <= m) updata(ls(k), l, m, L, R, v);
if (R > m) updata(rs(k), m + 1, r, L, R, v);
pushup(k);
}
void modify(int k, int l, int r, int x, int v) {
if (l == r) return mn[k] = v, void();
int m = l + r >> 1; pushdown(k);
if (x <= m) modify(ls(k), l, m, x, v);
else modify(rs(k), m + 1, r, x, v);
pushup(k);
}
int query(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) return mn[k];
int m = l + r >> 1, res = INF; pushdown(k);
if (L <= m) res = query(ls(k), l, m, L, R);
if (R > m) res = min(res, query(rs(k), m + 1, r, L, R));
return res;
}
int get(int k, int l, int r) {
if (l == r) return l;
int m = l + r >> 1; pushdown(k);
if (t[ls(k)] < 0) return get(ls(k), l, m);
else return get(rs(k), m + 1, r);
}
} T;
#undef ls
#undef rs
#define ls(k) t[k].ls
#define rs(k) t[k].rs
struct node { int ls, rs, s; } t[N << 7];
int ts, rt[N];
void updata(int &k, int o, int l, int r, int L, int R, int v) {
t[k = ++ts] = t[o]; int m = l + r >> 1;
if (L <= l && r <= R) return t[k].s += v, void();
if (L <= m) updata(ls(k), ls(o), l, m, L, R, v);
if (R > m) updata(rs(k), rs(o), m + 1, r, L, R, v);
}
int query(int k, int l, int r, int x) {
if (l == r) return t[k].s; int m = l + r >> 1;
if (x <= m) return query(ls(k), l, m, x) + t[k].s;
else return query(rs(k), m + 1, r, x) + t[k].s;
}
void init(signed n, const vector<int> &A) {
::n = n;
for (int i = 1; i <= n; i++) {
a[i] = A[i - 1]; int x = __lg(a[i]);
p[x].push_back(i), f[i][0] = x;
}
for (int j = 1; j < M; j++)
for (int i = 1; i + (1ll << j) - 1 <= n; i++)
f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
for (int w = 0; w < M; w++) {
if (!p[w].size()) continue; int m = 0;
for (auto i : p[w]) b[++m] = i, a[i] -= 1ll << w;
sort(b + 1, b + 1 + m,
[](int x, int y) { return a[x] < a[y]; });
for (int i = 1; i <= m; i++) rk[b[i]] = i;
T.build(1, 1, m); int pre = 0;
for (auto i : p[w]) {
T.updata(1, 1, m, rk[i], m, -1);
T.modify(1, 1, m, rk[i], i);
updata(rt[i], rt[pre], 1, n, 1, i, 1), pre = i;
if (T.t[1] >= 0) continue;
int x = T.get(1, 1, m), y = T.query(1, 1, m, 1, x);
T.updata(1, 1, m, rk[y], m, 1);
T.modify(1, 1, m, rk[y], INF);
updata(rt[i], rt[i], 1, n, 1, y, -1);
}
}
}
inline int qry(int l, int r) {
int k = __lg(r - l + 1);
return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int ask(signed l, signed r) {
int w = qry(l, r);
int L = lower_bound(p[w].begin(), p[w].end(), l) - p[w].begin();
int R = upper_bound(p[w].begin(), p[w].end(), r) - p[w].begin() - 1;
if (L > R) return (1ll << w) + r - l + 1;
else return (1ll << w) + r - l + 1 + query(rt[p[w][R]], 1, n, p[w][L]);
}
vector<int> askAll(signed q, const vector<signed> &l, const vector<signed> &r) {
vector <int> ans;
for (int i = 0; i < q; i++) ans.push_back(ask(l[i], r[i]));
return ans;
}
2026.8.1 组合计数与 dp 1
除第一题和 CF1967E1 外不是很推荐做,收获可能有限。
部分题目题解:
P10175 「OICon-02」Subtree Value
大约两年前 hjx 学长讲课讲了,但当时觉得很困难,现在不过是可以轻易理解的题目了。
暴力 dp 思路:枚举 \(|S|\),然后设 \(f_{i,j}\) 表示 \(i\) 子树内连通块大小为 \(j\) 的方案数,这样是 \(O(n^3)\) 的,瓶颈在于枚举 \(|S|\),其用处是为了处理权值中的 \(+|S|\) 这一项。
模数很特殊,有什么用?权值是乘积的形式,也就是如果 \(a_i+|S|\) 中有 \(V\) 项是 \(U\) 的倍数,那么对答案就没有贡献了。但是这个限制有些太弱了,优化效果为 \(0\)。
其实更好想到的思路是暴力的更进一步:在 dp 中记录有多少个选了 \(|S|\),最后再把系数乘上去,不过复杂度和第一个做法是一样的。
手动构造吧!乘法分配律的 trick。 将 \(|S|\) 拆成 \(Ux+y\)(\(a_i\) 是定值所以更优秀的是不拆开),那么权值就变成 \(\prod\limits_{i\in S} \left(Ux+(a_i+y)\right)\)。这样的好处是我们只需要记录有多少个选了 \(Ux\),最后得到 \(|S|\) 后再乘上贡献即可,同时 \(Ux\) 个数不超过 \(V\),因此复杂度可以接受,是 \(O(n^2UV^2)\) 的。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2005, M = 7;
vector <int> p[N];
int P = 1, a[N], siz[N], f[N][N][M], g[N][M], w, V;
inline int mod(int x) { return x >= P ? x - P : x; }
inline void add(int &x, int y) { x = mod(x + y); }
void dfs(int k) {
siz[k] = 1, f[k][1][0] = mod(a[k] + w), f[k][1][1] = 1;
for (auto i : p[k]) {
dfs(i); memcpy(g, f[k], sizeof(g));
memset(f[k], 0, sizeof(f[k]));
for (int x = 1; x <= siz[k]; x++)
for (int y = 0; y <= siz[i]; y++)
for (int p = 0; p < V; p++)
for (int q = 0; p + q < V; q++)
f[k][x + y][p + q] += g[x][p] * f[i][y][q];
siz[k] += siz[i];
for (int x = 1; x <= siz[k]; x++)
for (int y = 0; y < V; y++) f[k][x][y] %= P;
}
f[k][0][0] = 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, U, ans = 0; cin >> n >> U >> V;
for (int i = 1; i <= V; i++) P *= U;
for (int i = 2, x; i <= n; i++)
cin >> x, p[x].push_back(i);
for (int i = 1; i <= n; i++) cin >> a[i];
for (w = 0; w < U; w++) {
memset(f, 0, sizeof(f)), dfs(1);
for (int i = 1; i <= n; i++)
for (int j = !w; j * U + w <= siz[i]; j++)
for (int k = 0, s = 1; k < V; k++) {
add(ans, f[i][j * U + w][k] * s % P);
s = s * j * U % P;
}
}
cout << ans << '\n';
return 0;
}
不难的题,但是懵了一下。
根据 grid 2 经典操作,只记录到特殊点的贡献。设 \(f_{i,j}\) 表示到了第 \(i\) 个特殊点,目前到了恰好 \(j\) 个特殊点的方案数。每次 \(s\) 除以 \(2\) 所以有用的 \(j\) 是 \(O(\log V)\) 的。
枚举上一个特殊点 \(k\),但是无法钦定中间不经过任何点。一个想法是只管钦定的那些点,然后上二项式反演,但是发现由于我们只记了 \(\log V\) 的点所以实际的点数不好确定就不好反演。
另一个想法是直接还是维持恰好的状态定义,发现可以在每一步转移时,在不管中间是否经过点后直接令 \(f_{i,j}\) 减去 \(f_{n,j}\),每一步都这样做就是对的,不需要二项式反演了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 5, M = 25;
const int P = 1e9 + 7;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
int fac[N], inv[N];
inline void init() {
fac[0] = inv[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % P;
inv[N - 1] = qpow(fac[N - 1]);
for (int i = N - 2; i; i--)
inv[i] = inv[i + 1] * (i + 1) % P;
}
inline int C(int n, int m) {
if (n < 0 || m < 0 || n < m) return 0;
return fac[n] * inv[m] % P * inv[n - m] % P;
}
inline int get(int n, int m) {
return n < 0 || m < 0 ? 0 : C(n + m, n);
}
struct node { int x, y; } a[N];
int f[N][M];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int A, B, n, m; bool fl1 = 0, fl2 = 0;
cin >> A >> B >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i].x >> a[i].y;
fl1 |= a[i].x == 1 && a[i].y == 1;
fl2 |= a[i].x == n && a[i].y == m;
}
if (fl1) m = m + 1 >> 1;
else a[++n] = {1, 1};
if (A == 1 && B == 1) return cout << m << '\n', 0;
if (fl2) m = m + 1 >> 1;
else a[++n] = {A, B};
sort(a + 1, a + 1 + n, [&](node x, node y) {
return x.x ^ y.x ? x.x < y.x : x.y < y.y;
});
f[1][1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < M; j++)
for (int k = 0; k < i; k++)
f[i][j] = (f[i][j]+ f[k][j - 1] * get(a[i].x - a[k].x, a[i].y - a[k].y)) % P;
for (int j = 2; j < M - 1; j++)
f[i][j] = (f[i][j] + P - f[i][j + 1]) % P;
}
int sum = get(A - 1, B - 1), ans = 0;
for (int i = 2; i <= 22; i++) {
ans = (ans + f[n][i] * m) % P;
sum = (sum - f[n][i] + P) % P;
m = m + 1 >> 1;
}
cout<< (ans + sum) * qpow(get(A - 1, B - 1)) % P;
return 0;
}
CF1967E1 Again Counting Arrays (Easy Version)
爽做格点计数,虽然根号分治感觉不好想到。
对 \(a\) 的要求仅仅是存在一个合法的 \(b\),那么对于给定的一个 \(a\),尝试贪心构造出一个最优的(即最可能合法)的 \(b\) 出来:若 \(a_i=b_{i-1}+1\),则 \(b_i=b_{i-1}-1\),否则 \(b_i=b_{i-1}+1\),\(a\) 合法等价于这样构造出来的 \(b\) 全部非负。
这是一个贪心的过程,不好用“\(a\) 满足什么限制”来刻画。\(a\) 对应唯一的一个 \(b\),那不如直接枚举 \(b\),计算有多少个 \(a\) 对应这个 \(b\)。
最先想到的是 dp。设 \(f_{i,j}\) 表示考虑 \(b\) 的前 \(i\) 个位置,\(b_i=j\) 对应的合法 \(a\) 的方案数。转移小小分类一下:\(a_i=b_{i-1}+1=j+1\) 贡献 \(f_{i,j+1}\);\(a_i\ne b_{i-1}+1\) 则 \(a_i\) 有 \((m-1)\) 种选法,贡献 \(f_{i,j-1}\times (m-1)\),合起来:
注意到如果 \(j\ge m\),那么之后 \(b\) 一定是一路加上去一定合法对 \(a\) 没有限制了随便取(对答案的贡献:\(f_{i,m}\times m^{n-i}\)),贡献可以直接计算,所以只需要记录 \(j<m\),复杂度是 \(O(nm)\) 的。
过不去,怎么办。注意到这个 dp 转移的形式很特殊。如果敏锐的话可以直接看出来就是一个带限制的折线计数问题。
具体的,如果 \(j\) 到了 \(m\) 转移是不同的,所以要分两种情况:一种是中间已经碰到 \(m\) 了;一种是从始至终没有碰到 \(y=m\)。枚举终点 \((x,y)\)(其中前者是第一次碰到 \(y=m\) 的位置 \((i,m)\),后者是 \((n,i)\),那么都形如一个问题:从 \((0,b_0)\) 走到 \((x,y)\),每次向右上或右下走一步,如果向右上走的系数的 \(m-1\),不能碰到 \(y=m\) 和 \(y=-1\) 的方案数。
这个是经典的双线反射容斥,可以 \(O(\frac{n}{m})\) 计算。
大概说一下做法,设碰到 \(y=m\) 为事件 \(A\),碰到 \(y=-1\) 为事件 \(B\),\(f(S)\) 为钦定依次经历 \(S\) 中的事件后的答案。重复的事件视为一件事(多次碰到只用取最先碰到的那个点反射一下),答案为:
\[f(\varnothing)-f(A)-f(B)+f(AB)+f(BA)-f(ABA)-f(BAB)\cdots \]\(f(S)\) 可以 \(O(1)\) 计算(实际上就是终点依次关于 \(S\) 中每条线对称)。然后注意到一个线路最多只能碰到 \(O(\frac{n}{m})\) 次,所以单次双线容斥的复杂度就是 \(O(\frac{n}{m})\) 的。
还差个右上的系数。但发现知道了终点后往右上的次数是固定的,所以最后再乘上即可
注意到了什么?一个 \(O(nm)\),一个 \(O(\frac{n}{m})\),根号平衡!若 \(m\le \sqrt n\) 则做 dp,否则做反射容斥,那么复杂度就可以做到 \(O(n\sqrt n)\),足以通过本题(至少 easy version)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5 + 5, M = 505;
const int P = 998244353;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
int fac[N], inv[N];
inline void init() {
fac[0] = inv[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % P;
inv[N - 1] = qpow(fac[N - 1]);
for (int i = N - 2; i; i--)
inv[i] = inv[i + 1] * (i + 1) % P;
}
inline int C(int n, int m) {
if (n < 0 || m < 0 || n < m) return 0;
return fac[n] * inv[m] % P * inv[n - m] % P;
}
inline int get(int n, int m) { return C(n + m, n); }
int n, m, b0, f[2][M], p1[N], p2[N];
inline void solve1() {
memset(f, 0, sizeof(f));
int res = 0; f[0][b0] = 1;
for (int i = 1; i <= n; i++) {
int x = i & 1, y = x ^ 1;
memset(f[x], 0, sizeof(f[x]));
for (int j = 0; j <= m; j++) {
f[x][j] = f[y][j + 1];
if (j) f[x][j] = (f[x][j] + f[y][j - 1] * (m - 1)) % P;
}
res = (res + f[x][m] * p1[n - i]) % P; f[x][m] = 0;
}
for (int i = 0; i < m; i++) res = (res + f[n & 1][i]) % P;
cout << res << '\n';
}
inline int cal(int x, int y) {
return get(x + y >> 1, x - y >> 1);
}
inline int calc(int x, int y) {
if (b0 + y + x & 1) return 0;
int res = cal(x, y - b0);
for (int _ = 0; _ < 2; _++) {
int op = 1, now = _, X = x, Y = y;
while (1) {
Y = now ? 2 * m - Y : -2 - Y;
now ^= 1, op = -op;
int t = cal(X, Y - b0); if (!t) break;
res = (res + t * op + P) % P;
}
}
return res;
}
void solve2() {
int ans = 0;
for (int i = m - b0; i <= n; i++) {
int w = p1[n - i] * p2[m - b0 + i >> 1] % P;
ans = (ans + calc(i - 1, m - 1) * w) % P;
}
for (int i = max(b0 - n, 0ll); i < m && i <= n + b0; i++)
ans = (ans + calc(n, i) * p2[i - b0 + n >> 1]) % P;
cout << ans << '\n';
}
void solve() {
cin >> n >> m >> b0; p1[0] = p2[0] = 1;
for (int i = 1; i <= n; i++) {
p1[i] = p1[i - 1] * m % P;
p2[i] = p2[i - 1] * (m - 1) % P;
}
if (b0 >= m) return cout << p1[n] <<'\n', void();
if (m * m <= n) solve1();
else solve2();
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int T; cin >> T;
while (T--) solve();
return 0;
}
组合意义还是太厉害了,但是高精度是啥阴。
\(n>k\) 时显然无解。总的方案数显然是 \(k^n\)。
在系列最后加入一个位置 \(k+1\),将序列看成一个环,在环上一定合法,方案数是 \((k+1)^n\)。
然后断成序列,把加入的作为拿走。发现如果最后一个位置有人那么以它前一个为末尾就是不行的,否则可以,那么合法的应该就是总数的 \(\frac{k+1-n}{k+1}\),答案即为 \(\frac{(k+1)^{n-1}(k+1)}{k^n}\)。
这里解释了为什么要加入一个虚拟位置。如果不加的话,从一个位置断开无法判断是否合法,因为无法根据最后的局面判断究竟有没有数跨过了它。
几百万年没见高精度题了,但这题还好,因为 \(\gcd(k,k+1)=1\),所以只用算 \(\gcd(k+1,k^n)\),所有运算都只用高精度和单精度,还算好写。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2005;
struct BigInt {
int a[N], len;
void init(int x) {
memset(a, 0, sizeof(a)); len = 0;
while (x) a[++len] = x % 10, x /= 10;
if (!len) len = 1;
}
friend BigInt operator *(BigInt x, int y) {
for (int i = 1, w = 0; i <= x.len; i++) {
int t = x.a[i] * y + w;
x.a[i] = t % 10, w = t / 10;
if (i == x.len && w) x.len++;
}
return x;
}
friend BigInt operator /(BigInt x, int y) {
for (int i = x.len, s = 0; i; i--) {
s = s * 10 + x.a[i];
x.a[i] = s / y, s %= y;
}
while (x.len > 1 && !x.a[x.len]) x.len--;
return x;
}
friend int operator %(BigInt x, int y) {
int s = 0;
for (int i = x.len; i; i--)
s = (s * 10 + x.a[i]) % y;
return s;
}
void print(int op = 0) {
for (int i = len; i; i--) cout << a[i];
if (op == 1) cout << ' ';
if (op == 2) cout << '\n';
}
};
inline int gcd(BigInt x, int y) {
if (x % y == 0) return y;
else return __gcd(x % y, y);
}
inline void solve() {
int n, k; cin >> n >> k;
if (n > k) return cout << "0 1\n", void();
BigInt A, B; A.init(1), B.init(k + 1 - n);
for (int i = 1; i <= n; i++) A = A * k;
int g = gcd(A, k + 1 - n);
for (int i = 1; i < n; i++) B = B * (k + 1);
A = A / g, B = B / g; B.print(1); A.print(2);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
简单套路 SG 函数。
枚举分成的堆数 \(s\),设 \(x=\lfloor\frac{n}{s}\rfloor\),写出 SG 递推式:
\(sg(n)=\operatorname{mex}\{[x 为奇数]sg(s+1)\oplus [s-x为奇数]sg(s)\}\)$
套路性地整除分块,然后发现 \(x\in [l,r]\) 只用枚举前 \(eps\) 个就行了(直接分讨也可以,但枚举比较无脑),复杂度 \(O(n\sqrt n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int sg[N];
bool vis[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T, F; cin >> T >> F;
for (int i = F; i < N; i++) {
vector <int> p;
for (int l = 2; l <= i; l++) {
int w = i / l, r = i / w;
for (int j = l; j <= min(l + 4, r); j++) {
int t = 0;
if (i - j * w & 1) t ^= sg[w + 1];
if ((w + 1) * j - i & 1) t ^= sg[w];
vis[t] = true, p.push_back(t);
}
l = r;
}
while (vis[sg[i]]) sg[i]++;
for (auto i : p) vis[i] = false;
}
while (T--) {
int n, ans = 0; cin >> n;
for (int i = 1, x; i <= n; i++)
cin >> x, ans ^= sg[x];
cout << (!!ans) << ' ';
}
return 0;
}
2026.8.2 Day 12 组合计数与 dp 2
存在部分过于高深知识直接跳过,一开始两个是析合树性质或许是板子,可以学习,不需要会析合树的构造。写的题质量都不错。
部分题目题解:
CF1089I Interval-Free Permutations
析合树结构运用。有空把完整的构建啥学下。
就是计数有多少个排列满足其对应的析合树根为析点,且恰好有 \(n+1\) 个节点。
正难则反,容斥。分根为合点和析点来考虑。
若根为合点,则枚举第一段长度为 \(i\),剩下的段随便排(不用管合不合法,如果后面只有一段则显然是合点,否则后面一整段是一个连续段,与析点性质矛盾,就一定是合点)。
设 \(f_n\) 表示长度为为 \(n\) 且不存在任意 \([1,i]\) 为连续段的排列数,转移还是容斥一下:
然后长度为 \(n\) 的根为合点的数量就是 \(2\sum\limits_{i=1}^{n-1} f_i(n-i)!\),因为有顺序和倒序两种。
若根为析点,其实就可以直接把答案求出来了(就是最终的答案,因为后面转移也要用到这个)。去除根为合点的上面已经写了。去除根为析点,则枚举根的儿子个数 \(4\le i<n\),系数分为两部分:将 \(n\) 个儿子划分成 \(i\) 部分;将每一部分“离散化”并重排,使得不存在长度 \(>1\) 的连续段。前者的贡献 \(A_{n,i}\) 容易 \(O(n^3)\) 递推,后者发现其实就是 \(g_i\)。所以 \(g\) 转移为:
\(O(n^3)\) 预处理出 \(g\),每次 \(O(1)\) 回答即可。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 405;
int fac[N], P, f[N], g[N][N], ans[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T >> P; fac[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % P;
f[1] = 1;
for (int i = 2; i < N; i++) {
for (int j = 1; j < i; j++)
f[i] = (f[i] + f[j] * fac[i - j]) % P;
f[i] = (fac[i] - f[i] + P) % P;
}
g[0][0] = 1;
for (int i = 1; i < N; i++)
for (int j = 1; j <= i; j++)
for (int k = 1; k <= i; k++)
g[i][j] = (g[i][j] + g[i - k][j - 1] * fac[k]) % P;
ans[1] = 1, ans[2] = 2;
for (int i = 4; i < N; i++) {
for (int j = 1; j < i; j++)
ans[i] = (ans[i] + f[j] * fac[i - j]) % P;
ans[i] = ans[i] * 2 % P;
for (int j = 4; j < i; j++)
ans[i] = (ans[i] + g[i][j] * ans[j]) % P;
ans[i] = (fac[i] - ans[i] + P) % P;
}
while (T--) {
int n; cin >> n;
cout << ans[n] << '\n';
}
return 0;
}
就是上一题稍微延续一下。
从上一题开始,前面的部分省略。
同时还是容斥,求有多少个排列除 \([1,n]\) 外所有的连续段长度均 \(\le k\)。
若根为合点,最长的显然是去掉开头或去掉结尾,因此要求开头和结尾的长度都 \(\le n-k\)。可以枚举开头和结尾的长度,中间的同理随便填,那么就是:
若根为析点,要求只是儿子的长度全部 \(\le k\)。设 \(B_{n,i}\) 表示把 \(n\) 个点划分成 \(i\) 段且每段长度都不超过 \(k\) 的方案数,同样也可以 \(O(n^3)\) 有,那么贡献为:
这里直接用 \(g\) 是因为仅仅是重排 \(i\) 个儿子使得不存在连续段而没有被 \(k\) 有任何限制。
然后做完了,用 \(n!\) 减去上面两坨就是答案。复杂度仍为 \(O(n^3)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 405;
const int P = 1e9 + 7;
int fac[N], f[N], g[N][N], s[N][N], ans[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m; fac[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % P;
f[1] = 1;
for (int i = 2; i < N; i++) {
for (int j = 1; j < i; j++)
f[i] = (f[i] + f[j] * fac[i - j]) % P;
f[i] = (fac[i] - f[i] + P) % P;
}
g[0][0] = s[0][0] = 1;
for (int i = 1; i < N; i++)
for (int j = 1; j <= i; j++) {
for (int k = 1; k <= i; k++)
g[i][j] = (g[i][j] + g[i - k][j - 1] * fac[k]) % P;
for (int k = 1; k <= i && k <= m; k++)
s[i][j] = (s[i][j] + s[i - k][j - 1] * fac[k]) % P;
}
ans[1] = 1, ans[2] = 2;
for (int i = 4; i < N; i++) {
for (int j = 1; j < i; j++)
ans[i] = (ans[i] + f[j] * fac[i - j]) % P;
ans[i] = ans[i] * 2 % P;
for (int j = 4; j < i; j++)
ans[i] = (ans[i] + g[i][j] * ans[j]) % P;
ans[i] = (fac[i] - ans[i] + P) % P;
}
int res = 0;
for (int i = n - m; i <= m; i++)
for (int j = n - m; j <= n - i; j++)
res = (res + f[i] * f[j] % P * fac[n - i - j]) % P;
res = res * 2 % P;
for (int i = 4; i <= n; i++)
res = (res + ans[i] * s[n][i]) % P;
cout << (fac[n] - res + P) % P << '\n';
return 0;
}
稍微有点厉害,但又稍微没那么厉害。
看到字典序,马上就可以反应应该是枚举第一个小于原序列的位置。
如果这个位置在第一排,就是康托展开,后面每一行的方案数都是错排数 \(D(n)\),是平凡的。
如果不在第一排,假设在 \((i,j)\),填了 \(x<a_{i,j}\),那么后 \(n-i\) 排的方案数仍然为错排数,而第 \(i\) 行的 \(k\in (j,n]\) 则在不等于 \(a_{i,1\sim j}\) 的前提下还要满足不等于 \(a_{i-1,k}\),设 \(a_{i-1,j+1\sim n}\) 中有 \(L\) 个没有在 \(a_{i,1\sim j}\) 中,那么就是一个带了 \(L\) 个限制的 \(n-j\) 个数的错排 \(f_{n-j,L}\)。
关于 \(f_{i,j}\) 的求法:
若 \(i=j\),就是最平凡的错排问题,\(f_{i,i}=D(i)\)。
否则不妨设第 \(i\) 个数没有限制,分两种:一种是取了 \(j\) 个限制中的一个,使前面的限制减少一,方案数为 \(j\times f_{i-1,j-1}\);还有就是取非限制中的一个,为 \((i-j)\times f_{i-1,j}\),合起来:
\[f_{i,j}=j\times f_{i-1,j-1}+(i-j)\times f_{i-1,j} \]可以 \(O(n^2)\) 完成递推。
回到原问题,这样即可做到 \(O(n^3)\),但还不够。
枚举完 \(x\) 的贡献只与 \(i,j,L\) 有关,而 \(x\) 对 \(L\) 的影响只有为 \(0\) 或 \(-1\)。在 \(x\notin a_{i,1\sim j-1}\) 的前提下,若 \(x\in a_{i-1,j+1\sim n}\) 则 \(L\) 减少一,否则不变。
所以只需要分两类情况算有多少个 \(x\) 满足即可,这个推一下可以用树状数组维护。注意如果 \(a_{i-1,j}< a_{i,j}\) 的话 \(x\) 取 \(a_{i-1,j}\) 的贡献要扣掉。复杂度 \(O(n^2\log n)\)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2005;
const int P = 998244353;
struct BIT {
int c[N];
inline void add(int x, int y) {
for (; x < N; x += x & -x) c[x] += y;
}
inline int qry(int x) {
int res = 0;
for (; x; x -= x & -x) res += c[x];
return res;
}
} T1, T2;
int a[N][N], f[N][N], pw[N];
bool vis1[N], vis2[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) cin >> a[i][j];
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
f[i][i] = (i - 1) * (f[i - 1][i - 1] + f[i - 2][i - 2]) % P;
for (int j = 0; j < i; j++) {
f[i][j] = (i - j) * f[i - 1][j] % P;
if (j) f[i][j] = (f[i][j] + j * f[i - 1][j - 1]) % P;
}
}
pw[0] = 1;
for (int i = 1; i <= n; i++)
pw[i] = pw[i - 1] * f[n][n] % P;
int ans = 0;
for (int i = 1; i <= n; i++) T1.add(i, 1);
for (int i = 1; i <= n; i++) {
ans = (ans + T1.qry(a[1][i] - 1) * f[n - i][0] % P * pw[n - 1]) % P;
T1.add(a[1][i], -1);
}
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= n; j++)
T1.add(j, 1), T2.add(j, 1), vis1[j] = vis2[j] = true;
for (int j = 1; j <= n; j++) {
int x = a[i][j], y = a[i - 1][j];
if (vis1[y]) T2.add(y, -1), vis1[y] = false;
int a = T2.qry(n), b = T1.qry(x - 1), c = T2.qry(x - 1);
if (x > y && vis2[y]) b--;
ans = (ans + (b - c) * f[n - j][a] % P * pw[n - i]) % P;
if (a) ans = (ans + c * f[n - j][a - 1] % P * pw[n - i]) % P;
T1.add(x, -1), vis2[x] = false;
if (vis1[x]) T2.add(x, -1), vis1[x] = false;
}
}
cout << ans <<'\n';
return 0;
}
CF1264D2 Beautiful Bracket Sequence (hard version)
这为啥有 *2900 啊,感觉不难。
给定了一个字符串,那么答案就是 \(\max\limits_{i=1}^n \min(L_i,R_{i+1})\),其中 \(L_i\) 表示 \([1,i]\) 内 ( 的个数,\(R_i\) 表示 \([i,n]\) 内 ) 的个数。
但是保留这个形式无法在不确定是计数,并且如果枚举 \(i\) 的话也无法保证一种可能的替换方式只在一个 \(i\) 处被统计。这种题常见的思路就是钦定在某一个位置被统计。
发现这题有一个形式特别好:钦定在 \(L_i=R_{i+1}\) 的地方被统计,唯一性在于移动一步 \(L,R\) 中必然有一个变化。设 \([1,i]\) 内有 \(a\) 个 (、\(x\) 个 ?,\([i+1,n]\) 内有 \(b\) 个 ')'、\(y\) 个 ?,好处是当枚举左边 \(x\) 个问号替换 \(j\) 个变成 ( 后可以直接右边一定替换了 \(a+b-j\) 个位 ')'。
然后就是推式子环节了:
然后就可以 \(O(1)\) 计算单个 \(i\) 的贡献了,总复杂度线性。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 5;
const int P = 998244353;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
int fac[N], inv[N];
void init() {
fac[0] = inv[0] = 1;
for (int i = 1; i < N; i++)
fac[i] = fac[i - 1] * i % P;
inv[N - 1] = qpow(fac[N - 1]);
for (int i = N - 2; i; i--)
inv[i] = inv[i + 1] * (i + 1) % P;
}
inline int C(int n, int m) {
if (n < 0 || m < 0 || n < m) return 0;
return fac[n] * inv[m] % P * inv[n - m] % P;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); string s; cin >> s;
int a = 0, x = 0, b = 0, y = 0;
for (int i = 0; i < s.size(); i++)
b += s[i] == ')', y += s[i] == '?';
int ans = 0;
for (int i = 0; i < s.size(); i++) {
a += s[i] == '(', x += s[i] == '?';
b -= s[i] == ')', y -= s[i] == '?';
ans = (ans + a * C(x + y, y + b - a)) % P;
ans = (ans + x * C(x + y - 1, y + b - a - 1)) % P;
}
cout << ans << '\n';
return 0;
}
有点意思的 dp 题。
设 \(P_i=\dbinom{k}{i}p^i(1-p)^{k-i}\)。容易想到,设 \(f_{i,l,r}\) 表示第 \(i\) 层当前区间剩了 \([l,r]\) 的概率,但是加上转移高达 \(O(n^5)\)。
这个转移有点复杂似乎很难优化啊,发现容斥一下会变得简单一些:
其中 \(A=\sum_{l\le r} f_{i-1,l,r}\)。
然后似乎就可以开始动了。转移中的那一坨只和一个端点有关,同时这个过程理应是对称的(即区间 \([l,r]\) 等价于区间 \([n-r,n-l]\))。于是设 \(g_{i,r}=\sum\limits_{l\le r}f_{i,l,r}\),转移就有:
继续加码!令 \(S_{i,r}=\sum_{r'\le r}g_{i,r'}\):
差不多了,可以把 \(f\) 换成 \(g\) 了:
于是预处理 \(\sum\limits_{j=1}^x S_{i,j}\),\(\sum\limits_{j=1}^x S_{i,j}P(j)\) 即可 \(O(1)\) 转移,总复杂度 \(O(nm)\)。警示后人。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2005, M = 1e5 + 5;
const int P = 1e9 + 7;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
int prod[N], fac[M], inv[M];
inline void init() {
fac[0] = inv[0] = 1;
for (int i = 1; i < M; i++)
fac[i] = fac[i - 1] * i % P;
inv[M - 1] = qpow(fac[M - 1]);
for (int i = M - 2; i; i--)
inv[i] = inv[i + 1] * (i + 1) % P;
}
inline int C(int n, int m) {
return fac[n] * inv[m] % P * inv[n - m] % P;
}
int f[N][N], s[N][N], sum[N], ss[N][N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, A, B, k; cin >> n >> m >> A >> B >> k;
init(); int p = A *qpow(B) % P, pt = (P + 1 - p) % P;
for (int i = 0; i <= min(m, k); i++)
prod[i] = C(k, i) * qpow(p, i) % P * qpow(pt, k - i) % P;
for (int i = 0; i <= m; i++)
sum[i] = ((i ? sum[i - 1] : 0) + prod[i]) % P;
for (int l = 1; l <= m; l++)
for (int r = l; r <= m; r++)
f[1][r] = (f[1][r] + prod[l - 1] * prod[m - r]) % P;
for (int r = 1; r <= m; r++) {
s[1][r] = (s[1][r - 1] + f[1][r]) % P;
ss[1][r] = (ss[1][r - 1] + prod[r] * s[1][r]) % P;
}
for (int i = 2; i <= n; i++) {
int al = s[i - 1][m];
for (int r = 1; r <= m; r++) {
f[i][r] = (al - s[i - 1][m - r]) * sum[r - 1] - ss[i - 1][r - 1];
f[i][r] = (f[i][r] % P + P) * prod[m - r] % P;
}
for (int r = 1; r <= m; r++) {
s[i][r] = (s[i][r - 1] + f[i][r]) % P;
ss[i][r] = (ss[i][r - 1] + s[i][r] * prod[r]) % P;
}
}
cout << s[n][m] << '\n';
return 0;
}
思路是有借鉴意义的,实现不想清楚就是史。
\(n\) 太大 dp 是没有前途的,但是发现我们只关心第一个 \(i\) 出现在哪里,设 \(s(x)=\frac{10^x-1}{9}\)(即 \(x\) 个 \(1\)),运用一下“\(x\) 权值为 \(x\) 则将贡献拆成 \(\sum_x\sum _i [a_i\ge x]\)”的 trick,那么问题就变成:求有多少个长度为 \(8\) 的序列 \(a\),满足 \(m\mid s(n)+\sum s(a_i)\)(\(a_i\) 表示 \(i\) 第一次出现在从后往前数第 \(a_i\) 个,注意不能有前导零,所以钦定 \(a_1=n\))。
然后发现 \(s(x)\) 是有循环节的,反正最多是 \(m\) 乘上一个比较小的常数,所以可以设 \(f_{i,j}\) 表示选了 \(i\) 个 \(a\),当前和模 \(m\) 为 \(j\) 的方案数,是一个多重背包的形式,循环节内的可以一起转移拿组合数乘下。令 \(V=8\),则复杂度为 \(O(m^2V^2)\)。
一个想复杂的错误思路是,寻找 \(10^x\) 的循环节,但这样需要令 \(m\leftarrow m\times 9\) 来处理 \(/9\) 的影响,做复杂了而且跑不过去。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int P = 999911659;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
const int N = 5005, M = 15;
int fac[M], inv[M], a[N];
inline void init() {
fac[0] = inv[0] = 1;
for (int i = 1; i < 10; i++)
fac[i] = fac[i - 1] * i % P, inv[i] = qpow(fac[i]);
}
inline int C(int n, int m) {
int res = 1;
for (int i = 1; i <= m; i++)
res = (n - i + 1) % P * res % P;
return res * inv[m] % P;
}
int m, f[M][N], g[M][N];
inline int qpowm(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = res * a % m;
a = a * a % m, b >>= 1;
}
return res;
}
map <pair <int, int>, int> vis;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int n; cin >> n >> m;
int top = 0, now = 0, t = 1, fir = n + 1, cir = n + 1;
while (top < n) {
now = (now * 10 + 1) % m, t = t * 10 % m;
pair <int, int> tmp = {now, t};
if (vis[tmp]) {
fir = vis[tmp], cir = top - fir + 1;
break;
}
a[++top] = now, vis[tmp] = top;
}
f[0][0] = 1;
for (int i = 1; i <= top; i++) {
int s = 1;
if (i >= fir) s = (n - fir) / cir + (i - fir <= (n - fir) % cir);
memcpy(g, f, sizeof(f)), memset(f, 0, sizeof(f));
for (int j = 0; j < 9; j++)
for (int k = 0; k <= j; k++)
for (int p = 0; p < m; p++) {
int x = (p + a[i] * k) % m;
f[j][x] = (f[j][x] + g[j - k][p] * C(k + s - 1, k)) % P;
}
}
int ans = 0, w = 0;
if (n <= cir) w = a[n];
else w = a[(n - fir) % cir + fir];
for (int i = 0; i < 9; i++)
for (int j = 0; j < m; j++)
if ((j + w) % m == 0) ans = (ans + f[i][j]) % P;
cout << ans <<'\n';
return 0;
}
非常厉害的题,使我的大脑旋转。
若前面那个系数是 \(\frac{1}{n-1}\),发现这个东西的组合意义就是最后删去的那条边连接的点的权值积的期望,而这个东西恒为 \(1\)。但是系数是 \(\frac{1}{n}\),启示我们构造个类似的有组合意义的东西出来。
\(n\) 想到删点。但边怎么办呢?把边也看做一个点。但是这样的问题是,总点数变成了 \(2n-1\),也不符合 \(n\) 啊?最无敌的操作来了,在每个边下面挂 \(p-1\) 个虚拟点,那么总点数就是 \((n-1)(p-1)+2n-1\equiv n\pmod p\)!也可以视作往边下面挂了 -1 个点把这个点“消掉”了。
经过这个转化后,就可以找组合意义了:每次删一个点,但是由原问题的限制可以得到原先就存在的点一定要在其相邻的所有边的点删完后再删,答案就是求这个的概率。
相当于求边有根向和叶向的树的拓扑序树。两个方向不好求,考虑容斥,钦定一些根向的边变成叶向的,然后只要求一定满足这些叶向的边。看成只保留叶向的边,那么每个点的贡献系数就是 \(\frac{1}{新树上子树内的点个数}\),分母在原树上就是其所在的由根向边(原先的和钦定的)组成的联通块中在其子树内的点的个数。
dp 状态呼之欲出了:设 \(f_{u,i}\) 表示 \(u\) 子树内形成的联通块大小为 \(i\) 的概率。
对于边点,其到儿子的边本身就是叶的甚至不用容斥,就是一个背包合并的过程。
对于原先存在的点 \(u\),枚举其儿子 \(v\),一定是根向的边。如果当前根向边不被钦定,就不会形成联通块,就是直接 \(f_{u,i}=f_{u,i}\times \sum_j f_{v,j}\);否则像边点一样合并,但是要乘上 \(-1\) 的容斥系数:\(f_{u,i}=f_{u,i}-f_{u,x}\times f_{v,i-x}\)。
最后考虑 \(u\) 的贡献,要求是钦定的联通块中最先被删除的,所以有 \(f_{u,i}\leftarrow f_{u,i}\times \frac{1}{i}\)。
但是有问题!我们加了 \(p-1\) 个点,这咋转移?事实上,对于这 \(p-1\) 个点合上边点的大小模 \(p\) 意义下为 \(0\) 并且恒有 dp 值为 \(1\),所以根本造成不了影响!\(f_{u,i}\) 应该是直接继承 \(f_{v,i}\),但是还有个 \(u\) 的顺序所以得乘上 \(\frac{1}{i}\)。
你会发现这样甚至都不用显式对边点访问了,直接在转移时再把儿子额外乘上那个概率的分数即可。复杂度 \(O(n^2)\),好久没看到思路这么巧妙实现又这么优美的题了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5005;
const int P = 998244353;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
vector <int> p[N];
int inv[N], f[N][N], siz[N], g[N];
void dfs(int k, int fa) {
f[k][1] = siz[k] = 1;
for (auto i : p[k]) {
if (i == fa) continue; dfs(i, k);
memcpy(g, f[k], sizeof(g)), memset(f[k], 0, sizeof(f[k]));
for (int x = 1; x <= siz[k]; x++) {
int s = 0;
for (int y = 1; y <= siz[i]; y++) {
f[k][x + y] = (f[k][x + y] - g[x] * f[i][y] % P * inv[y] % P + P) % P;
s = (s + f[i][y] * inv[y]) % P;
}
f[k][x] = (f[k][x] + s * g[x]) % P;
}
siz[k] += siz[i];
}
for (int i = 1; i <= siz[k]; i++) f[k][i] = f[k][i] * inv[i] % P;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n;
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
p[u].push_back(v);
p[v].push_back(u);
}
for (int i = 1; i <= n; i++) inv[i] = qpow(i);
dfs(1, 0); int ans = 0;
for (int i = 1; i <= n; i++) ans += f[1][i];
cout << ans % P << '\n';
return 0;
}
2026.8.3 省选模拟赛 3
猎奇全 ds 场。
MXOJ P100262 [NOI--四十连测第十五套] -- T1 --王国
点分治 \(1\log\) 根本卡不过去,被两 \(\log\) 冲烂了。
std 做法是注意到可以直接统计到根的,然后减去 lca 到根的以及 lca 附近的一些东西,精细实现一下甚至可以直接做到除 lca 外线性。
MXOJ P100270 2025 -- [NOI--四十连测第十七套] -- T3 --brackets
写完 \(k=2\) 极限了,\(k=3\) 太史了口胡,不保证正确性。
\(k=1\) 是本质不同子串个数板子。
\(k=2\)。暴力 check 是要用栈,因此不难想到消消乐 trick。本质不同就是用 SA 的处理方法,从后往前扫,对左端点有一个右端点大于等于某个值的限制。
但是这题左右括号有区别,比消消乐更严格一些,还对右端点有限制:要求从左端点出发往后走每种括号前缀和都非负,是形如右端点小于等于 \(x\) 的限制,随便 st 表 + 倍增可以求出。还有就是如果消不动了后面一坨都不能要了,也是要求右端点小于等于某个值。
所以就是右端点要在一个区间内,直接在对应哈希值的 vector 上二分一下即可。
\(k=3\),开始口胡,应该没错。发现对于一个哈希值的 vector,如果是剥一层括号得到的深度一定只有最近的一段。随着右端点往右就是一段段取前缀 \(\max\)。这个可以直接上个线段树,应该也可以直接单调栈一下将相同权值的段并起来,查询时在大段上二分,边界上要进小段二分。
复杂度居然是高贵的单 \(\log\),3e5 目测应该咋写都不会被卡常。
放下 \(k\le 2\) 的代码。
#include <bits/stdc++.h>
using namespace std;
int n;
unordered_map <char, int> p;
namespace solve1 {
const int N = 1e6 + 5, M = 8;
int tr[N][M], lk[N], ts = 1, lst = 1, len[N];
inline void add(int c) {
int cur = ++ts, p = lst, q;
lst = cur, len[cur] = len[p] + 1;
while (p && !tr[p][c]) tr[p][c] = cur, p = lk[p];
if (!p) return lk[cur] = 1, void();
if (len[q = tr[p][c]] == len[p] + 1)
return lk[cur] = q, void();
int cl = ++ts; len[cl] = len[p] + 1;
lk[cl] = lk[q], lk[cur] = lk[q] = cl;
for (int i = 0; i < M; i++) tr[cl][i] = tr[q][i];
while (p && tr[p][c] == q) tr[p][c] = cl, p = lk[p];
}
void main() {
for (int i = 1; i <= ts; i++)
for (int j = 0; j < M; j++) tr[i][j] = 0;
lst =ts = 1;
for (int i = 1; i <= n; i++) {
char x; cin >> x; add(p[x]);
}
long long ans = 0;
for (int i = 1; i <= ts; i++)
ans += len[i] - len[lk[i]];
cout << ans << '\n';
}
}
typedef unsigned long long ull;
namespace solve2 {
const int base = 31;
const int N = 3e5 + 5, M = 20;
ull hsh[N], pw[N];
int a[N], rk[N], sa[N], id[N], t[N << 1], cnt[N];
inline bool cmp(int x, int y, int len) {
return t[x] == t[y] && t[x + len] == t[y + len];
}
void SA() {
int m = 10;
for (int i = 1; i <= max(n, m); i++) cnt[i] = t[i + n] = 0;
for (int i = 1; i <= n; i++) cnt[rk[i] = a[i] + 1]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i; i--) sa[cnt[rk[i]]--] = i;
for (int len = 1; len < n; len <<= 1) {
int ts = 0;
for (int i = n - len + 1; i <= n; i++) id[++ts] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > len) id[++ts] = sa[i] - len;
for (int i = 1; i <= m; i++) cnt[i] = 0;
for (int i = 1; i <= n; i++) cnt[rk[id[i]]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i; i--) sa[cnt[rk[id[i]]]--] = id[i];
m = 0; memcpy(t, rk, sizeof(t));
for (int i = 1; i <= n; i++)
rk[sa[i]] = (m += !cmp(sa[i], sa[i - 1], len));
if (m == n) break;
}
}
inline ull get(int l, int r) {
return hsh[r] - hsh[l - 1] * pw[r - l + 1];
}
inline int lcp(int x, int y) {
int l = 1, r = n - max(x, y) + 1, res = 0;
while (l <= r) {
int mid = l + r >> 1;
if (get(x, x + mid - 1) == get(y, y + mid - 1))
res = mid, l = mid + 1;
else r = mid - 1;
}
return res;
}
unordered_map <ull, int> p;
vector <int> f[N];
int s[N], mn[N], g[N][M];
void main() {
long long ans = 0;
for (int i = 1; i <= n; i++) {
char x; cin >> x; a[i] = ::p[x];
}
SA(); pw[0] = 1;
for (int i = 1; i <= n; i++) {
pw[i] = pw[i - 1] * base;
hsh[i] = hsh[i - 1] * base + a[i] + 1;
mn[i] = n + 1;
}
for (int _ = 0, x = 0, y = 1; _ < 3; _++) {
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1];
if (a[i] == x) s[i]++;
if (a[i] == y) s[i]--;
g[i][0] = s[i];
}
for (int j = 1; j < M; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
g[i][j] = min(g[i][j - 1], g[i + (1 << j - 1)][j - 1]);
for (int i = 1; i <= n; i++) {
int res = i;
for (int j = M - 1; ~j; j--)
if (res + (1 << j) - 1 <= n && g[res][j] >= s[i - 1])
res += 1 << j;
mn[i] = min(mn[i], res - 1);
}
x += 2, y += 2;
}
ull H = 0; int ts = 0, mx = n; p.clear();
for (int i = 1; i <= n; i++) f[i].clear();
stack <int> q; stack <ull> h; h.push(0);
for (int i = n; i >= 1; i--) {
if (!p[H]) p[H] = ++ts; f[p[H]].push_back(i);
if (a[i] & 1) q.push(a[i]), h.push(H = H * base + a[i] + 1);
else if (q.empty() || q.top() != a[i] + 1) mx = i - 1;
else q.pop(), h.pop(), H = h.top();
int l = rk[i] == n ? i : i + lcp(i, sa[rk[i] + 1]), r = min(mx, mn[i]);
if (l <= r ) {
int a = upper_bound(f[p[H]].begin(), f[p[H]].end(), l, greater <int>()) - f[p[H]].begin();
int b = lower_bound(f[p[H]].begin(), f[p[H]].end(), r, greater <int>()) - f[p[H]].begin();
ans += a - b;
}
}
cout << ans << '\n';
}
}
int main() {
freopen("brackets.in", "r", stdin);
freopen("brackets.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
p['('] = 0, p[')'] = 1, p['['] = 2;
p[']'] = 3, p['{'] = 4, p['}'] = 5;
int c, T; cin >> T >> c;
while (T--) {
int _; cin >> n >> _;
if (_ == 1) solve1::main();
if (_ == 2) solve2::main();
}
return 0;
}
2026.8.4 Day 13 数学综合 1
课件写得不是很好,建议看题解 / 补充资料。
部分题目题解:
Matrix tree 板子题,愿意当外向还是内向树做都可以。
#include <bits/stdc++.h>
using namespace std;
const int N = 305;
const int P = 1e4 + 7;
inline void add(int &x, int y) { (x += y) >= P && (x -= P); }
inline void sub(int &x, int y) { (x -= y) < 0 && (x += P); }
int n, a[N][N];
inline int det() {
int op = 1;
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
while (a[i][i]) {
int w = a[j][i] / a[i][i];
for (int k = i; k <= n; k++)
sub(a[j][k], w * a[i][k] % P);
swap(a[j], a[i]), op = -op;
}
swap(a[i], a[j]), op = -op;
}
int res = 1;
for (int i = 1; i <= n; i++)
res = res * a[i][i] % P;
return (res * op + P) % P;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m; cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
if (u == 1) u = n; else if (u == n) u = 1;
if (v == 1) v = n; else if (v == n) v = 1;
add(a[u][u], 1), sub(a[u][v], 1);
}
n--, cout << det() << '\n';
return 0;
}
牛牛数论题。
讲课介绍了二次剩余的性质,感觉非常优美啊,先写在这。
若 \(p\) 为奇素数:
- \(p\) 的二次剩余和非二次剩余(不考虑 \(0\)) 个数相同,均为 \(\frac{p-1}{2}\)。考虑到有 \(0< a,b<p,a^2\equiv b^2\pmod p\) 当且仅当 \(a=b\) 或 \(a=p-b\),因为有 \((a-b)(a+b)\equiv 0\pmod p\)。所以可以把 \(1\sim p-1\) 中的数两两配对,它们恰好覆盖了 \(\frac{p-1}{2}\) 个二次剩余,剩下的就是非二次剩余。
- 二次剩余更本质的刻画:\(p\) 必然存在原根 \(g\),\(g^{0\sim p-1}\) 覆盖了 \(0\sim p-1\),那么对于 \(g^{2k},k\ge 0\) 对应的就是二次剩余,\(g^{2k+1},k\ge 0\) 对应的就是非二次剩余。
- 二次剩余乘二次剩余仍是二次剩余;非二次剩余乘二次剩余仍是非二次剩余;非二次剩余乘非二次剩余会变成二次剩余。
直接用上一条原根的表示来刻画即可。- 勒让德符号:
\[\left(\dfrac{a}{p}\right)=\begin{cases}0,& a\mid p\\ 1,&a\nmid p\land[a 为 p 的二次剩余]\\-1, & a\nmid p\land[a 为 p 的非二次剩余] \end{cases} \]由上一条性质可知,勒让德符号是关于 \(a\) 的完全积性函数。
这题 \(p\) 不是质数感觉根本不可做啊!于是将 \(p\) 质因数分解一下分别做,然后中国剩余定理将所有 \(a,b,x\) 合成一下,答案应该是所有质因数的答案之积。
然后问题就转变为了 \(p\) 为奇素数的情况,联想到二次剩余。分讨一下。
若 \(x=0\)。相当于 \(a^2\equiv -b^2\pmod p\)。平凡地有 \(a=b=0\),否则转化为 \((\frac{a}{b})^2\equiv -1\pmod p\)。若 \(-1\) 为 \(p\) 的二次剩余, \(\Leftrightarrow(-1)^{\frac{p-1}{2}}\equiv 1\pmod p\Leftrightarrow p\bmod 4=1\),那么 \(\frac{a}{b}\) 就有两种取值,\((a,b)\) 对就有 \(2(p-1)\) 种;若 \(p\bmod 4=3\),则 \(\frac{a}{b}\) 无解。合上 \(a=b=0\) 的情况,有:
若 \(x\) 为二次剩余。不妨设 \(x\equiv t^2\pmod p\)(这里不是对 \(x\) 计数,所以随便取一个 \(t\) 即可,不需要乘以 \(2\))。同样先考虑 \(a=0\) 的平凡情况,那么 \(b\equiv \pm t\pmod p\),有两种。
否则两边同时除以 \(a^2\) 得到 \(1+(\frac{b}{a})^2\equiv (\frac{t}{a})^2\pmod p\),令 \(u=\frac{b}{a},v=\frac{t}{a}\),要求 \(1+u^2\equiv v^2\pmod p,v\ne 0\),那么 \((a,b)\) 与 \((u,v)\) 一一对应。
然后容易想到完全平方差公式:\((v-u)(v+u)\equiv -1\pmod p\)。再令 \(s=v-u,t=v+u\),若不考虑 \(v\ne 0\) 的限制,则 \((s,t)\) 与 \((u,v)\) 一一对应。显然有 \(t=s^{-1}\),而质数情况下每个数(除了 \(0\))逆元都存在,因此有 \(p-1\) 种,回过头,\((a,b)\) 也有 \(p-1\) 种。但别忘了去掉 \(v=0\) 的情况,此时有 \(u^2\equiv -1\pmod p\),和上文情况类似的,若 \(p\bmod 4=1\) 得扔掉两种。
最后也别忘了合上 \(a=0\) 的两种。因此:
最后是 \(x\) 不是二次剩余。但是这看上去没什么性质啊。发现上面 \(p\) 相同时的答案都相同,那么非二次剩余时是否也如此?
事实上证明并不难。因为等式两边同时乘上 \(k^2\),则变成:\((ak)^2+(bk)^2\equiv xk^2\pmod p\),而由上文的性质知道所有二次剩余之间和非二次剩余之间的答案都是相同的。而所有 \(a,b\) 一共有 \(p^2\) 对,去掉 \(x\) 为二次剩余的对数再去掉 \(x=0\) 的对数后再除以 \(\frac{p-1}{2}\) 就可以得到所有非二次剩余的答案了,算一下:
惊讶的发现其实和二次剩余的 \(x\) 是相同的。
做完了,预处理每个数的最小质因子,每次询问可以 \(O(\omega(p))\) 回答。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e7 + 5;
int pr[N], ts, mn[N];
bool vis[N];
void init() {
for (int i = 2; i < N; i++) {
if (!vis[i]) pr[++ts] = i, mn[i] = i;
for (int j = 1; j <= ts && i * pr[j] < N; j++) {
vis[i * pr[j]] = 1, mn[i * pr[j]] = pr[j];
if (i % pr[j] == 0) break;
}
}
}
inline int cal(int x, int p) {
if (x % p == 0) return p % 4 == 1 ? 2 * p - 1 : 1;
else return p % 4 == 1 ? p - 1 : p + 1;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int T; cin >> T;
while (T--) {
int p, x, ans = 1; cin >> p >> x;
while (p > 1) ans *= cal(x, mn[p]), p /= mn[p];
cout << ans << '\n';
}
return 0;
}
P3543 [POI 2012] WYR-Leveling Ground
真正好的数论题,无需用到高深的数论知识。
套路性地转化为差分。先不管必须一正一负配对的要求,对于每个位置 \(a_i\),先用 exgcd 求出 \(Ax_i+By_i=a_i\) 的 \(|x_i|+|y_i|\) 最小的 \((x_i,y_i)\),无解就全局无解。这个怎么求?就是注意到这个最小一定是 \(x,y\) 中的一个绝对值最小,稍微分讨一下就可以得到。
别忘了,要求 \(+A\) 和 \(-A\) 两两配对,即 \(S=\sum x_i =0\)。考虑在前面求出的那组的基础上进行调整,用一个堆每次取最小的调整代价来做,容易证明每次取全局最优是对的。
注意到只有在 \(x,y\) 任意一个正负性变化时调整的代价才会变化,这样只需要调整 \(O(n)\) 次。事实上,再观察一步,\(|\frac{S}{B}|\le n\)。根据上文的描述,有 \(|x_i|\le B\) 或 \(|y_i|\le A\)。前者显然有 \(S_1\le n_1B\),后者也有 \(|\frac{\sum a_i-S_2A}{B}|\le n_2A\),由 \(\sum a_i\) 得 \(S_2\le n_2B\),合起来就是 \(S\le nB\)。
所以甚至不要什么一次调整多步,每次调整一步就是正确的,很好写。复杂度 \(O(n\log V)\)。
注意差分数组要开到 \(n+1\),因为转化为差分可能有在最后一个位置减去某个数的操作。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
#define int long long
using namespace std;
inline void exgcd(int a, int b, int &x, int &y) {
if (!b) return x = 1, y = 0, void();
exgcd(b, a % b, y, x), y -= a / b * x;
}
const int N = 1e5 + 5;
int a[N], x[N], y[N];
priority_queue <pii, vector <pii>, greater <pii> > q;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, A, B; cin >> n >> A >> B;
int G = __gcd(A, B), X, Y;
int pa = A / G, pb = B / G; exgcd(pa, pb, X, Y);
for (int i = 1; i <= n; i++) cin >> a[i]; n++;
for (int i = n; i; i--) a[i] -= a[i - 1];
memset(x, 0x3f, sizeof(x)), memset(y, 0x3f, sizeof(y));
for (int i = 1; i <= n; i++) {
if (a[i] % G) { return cout << "-1\n", 0; };
int x = a[i] / G * X, y = a[i] / G * Y;
auto upd = [&]() {
if (abs(::x[i]) + abs(::y[i]) > abs(x) + abs(y))
::x[i] = x, ::y[i] = y;
} ;
x = (x % pb + pb) % pb, y = (a[i] - x * A) / B, upd();
x -= pb, y = (a[i] - x * A) / B, upd();
y = (y % pa + pa) % pa, x = (a[i] - y * B) / A, upd();
y -= pa, x = (a[i] - y * B) / A, upd();
}
int sum = 0, ans = 0;
for (int i = 1; i <= n; i++)
sum += x[i], ans += abs(x[i]) + abs(y[i]);
auto insert = [&](int i) {
int w = sum > 0 ? 1 : -1;
q.push({abs(x[i] - w * pb) + abs(y[i] + w * pa) - abs(x[i]) - abs(y[i]), i});
};
for (int i = 1; i <= n; i++) insert(i);
while (sum) {
pii t = q.top(); q.pop(); ans += t.fi;
int w = sum > 0 ? 1 : -1; sum -= pb * w;
x[t.se] -= w * pb, y[t.se] += w * pa, insert(t.se);
}
cout << ans / 2 << '\n';
return 0;
}
比较板的两部分拼起来,不用有一步有点新意/是 trival 的。
\(\gcd\) 看上去就很没办法做,直接反演成:
枚举 \(d\),相当于保留能被 \(d\) 整除的那些边,然后求所有生成树的 \(w\) 之和。
但是 matrix-tree 中求的是权值积啊,咋整?把每条边的权值看成一个 \(1+w_ix\) 的一次多项式,计算只用保留 \(\pmod{x^2}\) 的结果即可。直接看就是最终的一次项就是对应生成树内边的 \(w\) 和,套上点组合意义的话,拆贡献,对于每条边钦定在生成树内求剩下的方案数,而是选一次项的那一条边就是被钦定的边。
\(p\) 是质数因此可以进行多项式除法,写个高斯消元就可以了。但是比如 uoj 上就有毒瘤 hack,生成树个数为 \(998244353\) 个数而权值和不是,此时高消会出现某一列常数项全部为零的情况,但也没有很难解决,直接不管常数项只消一次项即可。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
#define int long long
using namespace std;
const int P = 998244353;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
inline int mod(int x) { return x >= P ? x - P : x; }
inline pii operator +(pii x, pii y) {
return {mod(x.fi + y.fi), mod(x.se + y.se)};
}
inline pii operator -(pii x, pii y) {
return {mod(x.fi + P - y.fi), mod(x.se + P - y.se)};
}
inline pii operator *(pii x, pii y) {
return {x.fi * y.fi % P, (x.fi * y.se + x.se * y.fi) % P};
}
inline pii operator /(pii x, pii y) {
int inv = qpow(y.fi), w = x.fi * inv % P;
return {w, (x.se - w * y.se % P + P) * inv % P};
}
const int N = 35, M = 2e5 + 5;
int pr[M], phi[M], ts; bool vis[M];
inline void init() {
phi[1] = 1;
for (int i = 2; i < M; i++) {
if (!vis[i]) pr[++ts] = i, phi[i] = i - 1;
for (int j = 1; j <= ts && i * pr[j] < M; j++) {
vis[i * pr[j]] = true;
if (i % pr[j] == 0) {
phi[i * pr[j]] = phi[i] * pr[j];
break;
} else phi[i * pr[j]] = phi[i] * phi[pr[j]];
}
}
}
int n, m;
pii a[N][N];
inline pii gauss() {
int op = 1;
for (int i = 1; i < n; i++) {
for (int j = i + 1; j < n; j++)
if (a[j][i].fi) {
swap(a[i], a[j]), op = P - op;
break;
}
for (int j = i + 1; j < n; j++) {
pii tmp = a[j][i] / a[i][i];
for (int k = i; k < n; k++)
a[j][k] = a[j][k] - tmp * a[i][k];
}
}
pii ans = {1, 0};
for (int i = 1; i < n; i++) ans = ans * a[i][i];
return ans * (pii){op, 0};
}
int u[M], v[M], w[M];
inline int solve(int x) {
memset(a, 0, sizeof(a));
for (int i = 1; i <= m; i++) {
if (w[i] % x) continue;
int x = u[i], y = v[i]; pii z = {1, w[i]};
a[x][x] = a[x][x] + z, a[y][y] = a[y][y] + z;
a[x][y] = a[x][y] - z, a[y][x] = a[y][x] - z;
}
return gauss().se;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int mx = 0, ans = 0; cin >> n >> m;
for (int i = 1; i <= m; i++)
cin >> u[i] >> v[i] >> w[i], mx = max(mx, w[i]);
for (int i = 1; i <= mx; i++) {
int cnt = 0;
for (int j = 1; j <= m; j++)
cnt += w[j] % i == 0;
if (cnt >= n - 1) ans = (ans + phi[i] * solve(i)) % P;
}
cout << ans << '\n';
return 0;
}
P5518 [MtOI2019] 幽灵乐团 / 莫比乌斯反演基础练习题
就是推式子,要从草稿纸上搬过来要肝爆了,题解一二篇写得都很好,type=2 第二篇做法很妙。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int P;
inline int qpow(int a, int b) {
int res = 1; b %= P - 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
inline int qpow(int a) { return qpow(a, P - 2); }
const int N = 1e5 + 5;
int pr[N], mu[N], ts, phi[N]; bool vis[N];
void init() {
mu[1] = phi[1] = 1;
for (int i = 2; i < N; i++) {
if (!vis[i]) pr[++ts] = i, mu[i] = -1, phi[i] = i - 1;
for (int j = 1; j <= ts && i * pr[j] < N; j++) {
vis[i * pr[j]] = true;
if (i % pr[j] == 0) {
phi[i * pr[j]] = phi[i] * pr[j];
mu[i * pr[j]] = 0; break;
} else {
mu[i * pr[j]] = -mu[i];
phi[i * pr[j]] = phi[i] * (pr[j] - 1);
}
}
}
}
namespace solve1 {
int f[N], inv[N], fac[N];
void init() {
fac[0] = 1;
for (int i = 1; i < N; i++) f[i] = 1;
for (int i = 1; i < N; i++) {
int inv = qpow(i);
fac[i] = fac[i - 1] * i % P;
for (int j = i; j < N; j += i) {
if (mu[j / i] == 1) f[j] = f[j] * i % P;
if (mu[j / i] == -1) f[j] = f[j] * inv % P;
}
}
f[0] = inv[0] = 1; ;
for (int i = 1; i < N; i++)
f[i] = f[i - 1] * f[i] % P, inv[i] = qpow(f[i]);
}
inline int cal1(int A, int B, int C) {
return qpow(fac[A], B * C);
}
inline int cal2(int A, int B, int C) {
int up = min(A, B), ans = 1;
for (int i = 1; i <= up; i++) {
int j = min(A / (A / i), B / (B / i));
int w = f[j] * inv[i - 1] % P;
ans = ans * qpow(w, (A / i) * (B / i)) % P;
i = j;
}
return qpow(ans, C);
}
inline int solve(int A, int B, int C) {
return cal1(A, B, C) * cal1(B, A, C) % P
* qpow(cal2(A, B, C) * cal2(A, C, B) % P) % P;
}
}
namespace solve2 {
int prod[N], f[N], inv[N];
void init() {
prod[0] = 1;
for (int i = 1; i < N; i++) f[i] = 1;
for (int i = 1; i < N; i++) {
prod[i] = prod[i - 1] * qpow(i, i) % P;
int inv = qpow(i);
for (int j = i; j < N; j += i) {
if (mu[j / i] == 1) f[j] = f[j] * i % P;
if (mu[j / i] == -1) f[j] = f[j] * inv % P;
}
f[i] = qpow(f[i], i * i);
}
f[0] = inv[0] = 1;
for (int i = 1; i < N; i++)
f[i] = f[i - 1] * f[i] % P, inv[i] = qpow(f[i]);
}
inline int S(int n) {
return n * (n + 1) / 2 % (P - 1);
}
inline int cal1(int A, int B, int C) {
int res = qpow(prod[A], S(B) * S(C) );
return qpow(prod[A], S(B) * S(C) );
}
inline int cal2(int A, int B, int C) {
int ans = 1;
for (int i = 1; i <= min(A, B); i++) {
int j = min(A / (A / i), B / (B / i));
int w = f[j] * inv[i - 1] % P;
ans = ans * qpow(w, S(A / i) * S(B / i) ) % P;
i = j;
}
return qpow(ans, S(C));
}
inline int solve(int A, int B, int C) {
return cal1(A, B, C) * cal1(B, A, C) % P
* qpow(cal2(A, B, C) * cal2(A, C, B) % P) % P;
}
}
namespace solve3 {
int s[N];
void init() {
for (int i = 1; i < N; i++)
s[i] = s[i - 1] + phi[i];
}
int solve(int A, int B, int C) {
int ans = 1;
for (int i = 1; i <= min({A, B, C}); i++) {
int j = min({A / (A / i), B / (B / i), C / (C / i)});
int res = solve1::solve(A / i, B / i, C / i);
ans = ans * qpow(res, s[j] - s[i - 1]) % P;
i = j;
}
return ans;
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
init(); int T; cin >> T >> P;
solve1::init(); solve2::init(); solve3::init();
while (T--) {
int A, B, C; cin >> A >> B >> C;
cout << solve1::solve(A, B, C) << ' ';
cout << solve2::solve(A, B, C) << ' ';
cout << solve3::solve(A, B, C) << '\n';
}
return 0;
}
2026.8.5 day14 数学综合 2
比较复杂(?)的算法偏板子。
部分题目题解:
Pólya 定理板子。
二合一问题。显然要先算出戒指有多少种,再算项链有多少种。
戒指。一共有 \(m\) 种置换,根据数论知识可以得到,旋转 \(i(1\le i\le m)\) 格则有 \(\gcd(m,i)\) 个等价类,于是可以写出方案数,然后简单化下式子:
这个直接暴力枚举因数暴力算欧拉函数就可以做了,求出来的答案设为 \(k\)。
项链。放了一个挂件因此不用考虑旋转相同,就是给环染色使得相邻两个颜色不同的方案数。
先不管第一个和最后一个,算出来是 \(k(k-1)^{n-1}\),然后扣掉最后一个和第一个钦定相同的答案,设 \(S(n)\) 表示长度为 \(n\) 的方案数,可以写出递推式:\(S(n)=k(k-1)^{n-1}-S(n-1)\)。
这个展开,其实就是高中基础数列题,推一下可以得到是 \((k-1)^n+(-1)^k(c-1)\)。
还没做完。这题 \(P\) 很小,因此在前面 Pólya 的时候可能会碰到 \(m\) 是 \(P\) 的倍数而不存在逆元的情况。解决办法是此时对 \(\bmod\ P^2\) 取模,Pólya 保证了求出来一定是 \(m\) 也一定是 \(P\) 的倍数,除以 \(P\) 乘上 \((\frac{m}{P})^{-1}\) 后再对 \(P\) 取模即可,注意要开 __int128。
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef __int128 i128;
const int P = 3214567, Mod = P * P;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
inline int Qpow(i128 a, int b) {
i128 res = 1;
while (b) {
if (b & 1) res = res * a % Mod;
a = a * a % Mod, b >>= 1;
}
return res;
}
inline int phi(int n) {
int ans = n;
for (int i = 2, t = n; i * i <= t; i++)
if (n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0) n /= i;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}
inline int solve(int n, int m) {
int ans = 0;
for (int i = 1; i * i <= n; i++)
if (n % i == 0) {
ans = (ans + (i128)Qpow(m, i) * phi(n / i)) % Mod;
if (i * i != n) ans = (ans + (i128)Qpow(m, n / i) * phi(i)) % Mod;
}
return ans;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, c; cin >> n >> m >> c; c = solve(m, c);
if (m % P ) c = c % P * qpow(m) % P;
else c = c / P * qpow(m / P) % P;
cout << (qpow(c - 1, n) + qpow(P - 1, n) * (c - 1)) % P;
return 0;
}
其实用到的是 burnside,但我说 Pólya 也叫 burnside。
这题的置换就是一个排列,枚举太不现实了,并且其实并不好直接算出答案。经由定理的推导过程的其实,考虑假如确定了所有置换环的情况,答案是多少。
对于置换环内部,给每个点重编号一下,设环长为 \(c\),则对于边 \((i,j)\),设其权值为 \(\min(|i-j|,c-|i-j|)\),那么所有权值相同的边都处在同一个等价类中,不难证,因此置换环内部可以自由染色的边的数量就是 \(\lfloor\frac{c}{2}\rfloor\)。
为对于置换环之间,设两个置换环长度为 \((a,b)\),那么根据数论知识,一个从 \(A\) 上一个点走出的边要走 \(\operatorname{lcm}(a,b)\) 步才会回到原点,所以等价类数量是 \(\frac{ab}{\operatorname{lcm}(a,b)}\) 的。
发现数量只和每个环的大小有关。对于一个有 \(c_1,c_2,\cdots c_k\) 大小的置换环的排列,其染色方案数为:\(m^{\sum_i (\lfloor \frac{c_i}{2}\rfloor +\sum _{j<i}\gcd(c_i,c_j))}\)。
于是不难想到枚举 \(c\) 数组,复杂度是拆分数的,搜一下发现可以接受。问题就变成了给定一个置换环局面,有多少排列对应着这个局面。
摁住置换环,随便填,是 \(n!\) 的。每个置换环都可以旋转,所以要除掉 \(\prod c_i\)。同时,相同长度的置换环之间本质相同,设长度为 \(i\) 的有 \(s_i\) 个,则还要除掉 \(\prod {s_i}!\)。
注意到 burnside 和方案数的 \(n!\) 可以约掉。最后的答案:
直接搜即可,在过程中就可以把各个部分算出来。比板子还短,有什么头猪吗。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, m, P;
inline int qpow(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
inline int qpow(int a) { return qpow(a, P - 2); }
const int N = 105;
int b[N], ans;
void dfs(int now, int ts, int prod, int c, int mul) {
if (now > n) {
ans = (ans + qpow(prod) * qpow(m, mul)) % P;
return;
}
for (int i = max(1ll, b[ts - 1]); i <= n - now + 1; i++) {
b[ts] = i; int s = i/ 2;
for (int j = 1; j < ts; j++)
s += __gcd(b[ts], b[j]);
int tc = b[ts] == b[ts - 1] ? c + 1 : 1;
dfs(now + i, ts + 1, prod * tc % P * i % P, tc, mul + s);
}
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> P;
dfs(1, 1, 1, 0, 0), cout << ans << '\n';
return 0;
}
挺厉害的不知道什么题。
看到绝对值,想到拆值。具体地,枚举 \(x\),\(\le x\) 设为 \(0\),否则设为 \(1\),那么相邻两位如果不同的话就会对答案有 \(1\) 的代价。
显然再对每一位拆贡献。做 \(m\) 次操作,发现每一次会变成什么样的方案数只和上一次长什么有关(\(00,01/10,11\)),于是可以把贡献写成 \(3\times 3\) 的矩阵矩阵加速 \(m\) 次操作,最后统计对多少个 \(x\) 有贡献也是容易的。复杂度 \(O(a^3n\log m)\)。
来个更牛的做法。上述还要转化并且利用到了绝对值的做法,有没有更通用的做法?
答:有的。设当前考虑 \(a_i=A,a_{i+1}=B\),由于任何交换都有可能,因此除 \(A,B\) 外的数到达 \(i/i+1\) 的概率都是相同的,直接将它们看做 \(C\),那么状态有 \(AB,BA,AC,CA,BC,CB,CC\) 七种,直接对着七种跑矩阵快速幂即可。七种比较好写而且显然不会过不了,愿意的话还可以继续压缩(本质应该就四种)。
算答案时可以所有对的答案是容易算的。注意这样求出来的是所有 \(C\) 方案的总和,上文说过每个都是相等的,因此要 \(AB/C\) 要除以 \(n-2\),\(CC\) 要除以 \((n-2)(n-3)\)。
复杂度 \(O(n+a^3\log m),a=7\),可以看做高贵的线性。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 5, M = 7;
const int P = 998244353;
struct matrix {
int a[M][M];
void init() {
for (int i = 0; i < M; i++) a[i][i] = 1;
}
friend matrix operator *(matrix x, matrix y) {
matrix z{};
for (int i = 0; i < M; i++)
for (int k = 0; k < M; k++)
for (int j = 0; j < M; j++)
z.a[i][j] = (z.a[i][j] + x.a[i][k] * y.a[k][j]) % P;
return z;
}
};
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
inline matrix qpow(matrix a, int b) {
matrix res{}; res.init();
while (b) {
if (b & 1) res = res * a;
a = a * a , b >>= 1;
}
return res;
}
int n, c[N], b[N];
inline int sum(int l, int r) {
return (l + r) * (r - l + 1) / 2 % P;
}
inline int cal(int x) {
return ((x * (2 * x - n) - sum(1, x) + sum(x + 1, n)) % P + P) % P;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m, ans = 0, sum = 0; cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> c[i], sum += (b[i] = cal(c[i]));
sum %= P;
matrix a, f{};
{
int w = (n - 2) * (n - 3) / 2 % P;
a = { {{w, 1, n - 2, 0, 0, n - 2, 0},
{1, w, 0, n - 2, n - 2, 0, 0},
{1, 0, w + n - 3, 1, 1, 0, n - 3},
{0, 1, 1, w + n - 3, 0, 1, n - 3},
{0, 1, 1, 0, w + n - 3, 1, n - 3},
{1, 0, 0, 1, 1, w + n - 3, n - 3},
{0, 0, 1, 1, 1, 1, w + 2 * (n - 4) + 1}} };
}
a = qpow(a, m); f.a[0][0] = 1, f = f * a;
for (int i = 1, inv = qpow(n - 2), inv2 = qpow(n - 3); i < n; i++) {
int w = abs(c[i] - c[i + 1]);
ans = (ans + (f.a[0][0] + f.a[0][1]) * w) % P;
ans = (ans + (f.a[0][2] + f.a[0][3]) * (b[i] - w) % P * inv) % P;
ans = (ans + (f.a[0][4] + f.a[0][5]) * (b[i + 1] - w) % P * inv) % P;
if (n > 2) ans = (ans + f.a[0][6] * (sum - 2 * (b[i] + b[i + 1] - w)) % P * inv % P * inv2) % P;
}
cout << (ans % P + P) % P << '\n';
return 0;
}
P3232 [HNOI2013 / JSOI2013] 游走
随机游走板子。
显然先求点的期望经过次数:
高斯消元。\((u,v)\) 的期望就是 \([u\ne n]\frac{f_u}{deg_u}+[v\ne n]\frac{f_v}{deg_v}\),大期望对小编号即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
const double eps = 1e-8;
int n, deg[N], u[N], v[N];
double a[N][N];
struct edge { int u, v; double w; } ;
void gauss() {
for (int i = 1; i <= n; i++) {
for (int j = i; j <= n; j++)
if (fabs(a[j][i]) > eps) {
swap(a[i], a[j]); break;
}
for (int j = n + 1; j >= i; j--) a[i][j] /= a[i][i];
for (int j = i + 1; j <= n; j++)
for (int k = n + 1; k >= i; k--)
a[j][k] -= a[j][i] * a[i][k];
}
for (int i = n; i; i--)
for (int j = i + 1; j <= n; j++)
a[i][n + 1] -= a[i][j] * a[j][n + 1];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m; cin >> n >> m; vector <edge> e;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
deg[u]++, deg[v]++, e.push_back({u, v, 0});
}
a[1][n + 1] = 1;
for (int i = 1; i <= n; i++) a[i][i] = 1;
for (auto [u, v, w] : e) {
if (v ^ n) a[v][u] -= 1.0 / deg[u];
if (u ^ n) a[u][v] -= 1.0 / deg[v];
}
gauss();
for (auto &[u, v, w] : e) {
if (u ^ n) w += a[u][n + 1] / deg[u];
if (v ^ n) w += a[v][n + 1] / deg[v];
}
sort(e.begin(), e.end(), [](edge x, edge y) {
return x.w > y.w; });
double ans = 0;
for (int i = 1; i <= m; i++)
ans += i * e[i - 1].w;
printf("%.3lf", ans);
return 0;
}

浙公网安备 33010602011771号