[TOC]

常用

快读快写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
class Input {
#define MX 1000000
private :
char buf[MX], *p1 = buf, *p2 = buf;
inline char gc() {
if(p1 == p2) p2 = (p1 = buf) + fread(buf, 1, MX, stdin);
return p1 == p2 ? EOF : *(p1 ++);
}
public :
Input() {
#ifdef Open_File
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
#endif
}
template <typename T>
inline Input& operator >>(T &x) {
x = 0; int f = 1; char a = gc();
for(; ! isdigit(a); a = gc()) if(a == '-') f = -1;
for(; isdigit(a); a = gc())
x = x * 10 + a - '0';
x *= f;
return *this;
}
inline Input& operator >>(char &ch) {
while(1) {
ch = gc();
if(ch != '\n' && ch != ' ') return *this;
}
}
inline Input& operator >>(char *s) {
int p = 0;
while(1) {
s[p] = gc();
if(s[p] == '\n' || s[p] == ' ' || s[p] == EOF) break;
p ++;
}
s[p] = '\0';
return *this;
}
#undef MX
} Fin;

class Output {
#define MX 1000000
private :
char ouf[MX], *p1 = ouf, *p2 = ouf;
char Of[105], *o1 = Of, *o2 = Of;
void flush() { fwrite(ouf, 1, p2 - p1, stdout); p2 = p1; }
inline void pc(char ch) {
* (p2 ++) = ch;
if(p2 == p1 + MX) flush();
}
public :
template <typename T>
inline Output& operator << (T n) {
if(n < 0) pc('-'), n = -n;
if(n == 0) pc('0');
while(n) *(o1 ++) = (n % 10) ^ 48, n /= 10;
while(o1 != o2) pc(* (--o1));
return *this;
}
inline Output & operator << (char ch) {
pc(ch); return *this;
}
inline Output & operator <<(const char *ch) {
const char *p = ch;
while( *p != '\0' ) pc(* p ++);
return * this;
}
~Output() { flush(); }
#undef MX
} Fout;

#define cin Fin
#define cout Fout
#define endl '\n'

常用缺省源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <bits/stdc++.h>

using namespace std;

#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)
#define debug(...) fprintf (stderr, __VA_ARGS__)

using i64 = long long;

const int P = 998244353;
inline int mod(int x) { return x + (x >> 31 & P); }
inline void sub(int &x, int y) { x = mod(x - y); }
inline void pls(int &x, int y) { x = mod(x + y - P); }
inline int add(int x, int y) { return mod(x + y - P); }
inline int dec(int x, int y) { return mod(x - y); }
inline int power(int x, int k) {
int res = 1; if (k < 0) k += P - 1;
while (k) { if (k & 1) res = 1ll * res * x % P; x = 1ll * x * x % P; k >>= 1; }
return res;
}

void solve() {

}

int main() {
std :: ios :: sync_with_stdio(false);
int Case; cin >> Case;
while (Case --) solve();
return 0;
}

数据结构

哈希表

gp_hash_table

1
2
3
4
5
6
7
#include <bits/extc++.h>

using __gnu_pbds :: gp_hash_table;

gp_hash_table <int, int> mp;

mp.find (x) != mp.end() // 注意mp[x] 会创建 x 元素

朴素实现

上面这个解决不了基本也得现场写。

用邻接表写比较快。

字符串哈希

单哈希

1
2
3
4
5
6
7
8
9
10
11
12
13
template <int P> 
struct String_hash { // s.size() > 1 !!!
std :: vector<int> pw, f;
const int bs = 233;
void init (std :: string s) { // [0, n - 1]
pw.resize (s.size() + 1); f.resize (s.size() + 1);
pw[0] = 1;
for (int i = 1; i <= (int) s.size(); i ++) pw[i] = 1ll * pw[i - 1] * bs % P;
f[0] = 0;
for (int i = 1; i <= (int) s.size(); i ++) f[i] = (1ll * f[i - 1] * bs + s[i - 1]) % P;
}
inline int get (int l, int r) { return (f[r] - 1ll * f[l - 1] * pw[r - l + 1] % P + P) % P; }
} ;

这个是单哈希, 双哈希建俩表就行了。

自然溢出

1
2
3
4
5
6
7
8
9
10
11
12
struct unlimit_hash { // s.size() > 1 !!!
std :: vector<ull> pw, f;
const ull bs = 233;
void init (std :: string s) { // [0, n - 1]
pw.resize (s.size() + 1); f.resize (s.size() + 1);
pw[0] = 1;
for (int i = 1; i <= (int) s.size(); i ++) pw[i] = pw[i - 1] * bs;
f[0] = 0;
for (int i = 1; i <= (int) s.size(); i ++) f[i] = f[i - 1] * bs + s[i - 1];
}
inline ull get (int l, int r) { return f[r] - f[l - 1] * pw[r - l + 1]; }
} ;

线段树

区间历史最值, 区间取min

来自洛谷模板。

给出一个长度为 $n$ 的数列 $A$,同时定义一个辅助数组 $B$,$B$ 开始与 $A$ 完全相同。接下来进行了 $m$ 次操作,操作有五种类型,按以下格式给出:

  • 1 l r k:对于所有的 $i\in[l,r]$,将 $A_i$ 加上 $k$($k$ 可以为负数)。
  • 2 l r v:对于所有的 $i\in[l,r]$,将 $A_i$ 变成 $\min(A_i,v)$。
  • 3 l r:求 $\sum_{i=l}^{r}A_i$。
  • 4 l r:对于所有的 $i\in[l,r]$,求 $A_i$ 的最大值。
  • 5 l r:对于所有的 $i\in[l,r]$,求 $B_i$ 的最大值。

在每一次操作后,我们都进行一次更新,让 $B_i\gets\max(B_i,A_i)$。

数据规模与约定

  • 对于全部测试数据,保证 $1\leq n,m\leq 5\times 10^5$,$-5\times10^8\leq A_i\leq 5\times10^8$,$op\in[1,5]$,$1 \leq l\leq r \leq n$,$-2000\leq k\leq 2000$,$-5\times10^8\leq v\leq 5\times10^8$。

下面是 2log 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <bits/stdc++.h>

using namespace std;

const int N = 5e5 + 10;
const int INF = 1e9;

using ll = long long;

struct Node {
Node *ls, *rs;
int l, r;
int add1, d_add1;
int add2, d_add2;
ll sum;
int mx1, mx2, cmx, d_mx1;
Node() {}
Node(int _l, int _r) : l(_l), r(_r), ls(NULL), rs(NULL) {}
void upd() {
sum = ls -> sum + rs -> sum;
d_mx1 = max(ls -> d_mx1, rs -> d_mx1);
if(ls -> mx1 == rs -> mx1) {
mx1 = ls -> mx1;
mx2 = max(ls -> mx2, rs -> mx2);
cmx = ls -> cmx + rs -> cmx;
}
else if(ls -> mx1 > rs -> mx1) {
mx1 = ls -> mx1;
mx2 = max(ls -> mx2, rs -> mx1);
cmx = ls -> cmx;
}
else {
mx1 = rs -> mx1;
mx2 = max(ls -> mx1, rs -> mx2);
cmx = rs -> cmx;
}
}
void pushtag(int k1, int d_k1, int k2, int d_k2) {
sum += 1ll * k1 * cmx + 1ll * (r - l + 1 - cmx) * k2;
d_add1 = max(d_add1, add1 + d_k1);
d_mx1 = max(d_mx1, mx1 + d_k1);
mx1 += k1; add1 += k1;
d_add2 = max(d_add2, add2 + d_k2);
if(mx2 != - INF) mx2 += k2;
add2 += k2;
}
void pushdown() {
int mx = max(ls -> mx1, rs -> mx1);
if(ls -> mx1 == mx)
ls -> pushtag(add1, d_add1, add2, d_add2);
else
ls -> pushtag(add2, d_add2, add2, d_add2);
if(rs -> mx1 == mx)
rs -> pushtag(add1, d_add1, add2, d_add2);
else
rs -> pushtag(add2, d_add2, add2, d_add2);
add1 = d_add1 = add2 = d_add2 = 0;
}
void modify1(int L, int R, int k) {
if(L <= l && r <= R) {
pushtag(k, k, k, k);
return ;
}
pushdown();
int mid = (l + r) >> 1;
if(L <= mid) ls -> modify1(L, R, k);
if(R > mid) rs -> modify1(L, R, k);
upd();
}
void modify2(int L, int R, int k) {
if(k >= mx1) return ;
if(L <= l && r <= R && k > mx2) {
pushtag(k - mx1, k - mx1, 0, 0); return ;
}
pushdown();
int mid = (l + r) >> 1;
if(L <= mid) ls -> modify2(L, R, k);
if(R > mid) rs -> modify2(L, R, k);
upd();
}
ll query3(int L, int R) {
if(L <= l && r <= R) return sum;
pushdown();
ll sum = 0;
int mid = (l + r) >> 1;
if(L <= mid) sum += ls -> query3(L, R);
if(R > mid) sum += rs -> query3(L, R);
return sum;
}
int query4(int L, int R) {
if(L <= l && r <= R) return mx1;
pushdown();
int mid = (l + r) >> 1;
int ans = -INF;
if(L <= mid) ans = max(ans, ls -> query4(L, R));
if(R > mid) ans = max(ans, rs -> query4(L, R));
return ans;
}
int query5(int L, int R) {
if(L <= l && r <= R) return d_mx1;
pushdown();
int mid = (l + r) >> 1;
int ans = -INF;
if(L <= mid) ans = max(ans, ls -> query5(L, R));
if(R > mid) ans = max(ans, rs -> query5(L, R));
return ans;
}
} ;

Node *build(int l, int r, int *a) {
Node *x = new Node(l, r);
x -> add1 = x -> d_add1 = x -> add2 = x -> d_add2 = 0;
if(l == r) {
x -> sum = x -> mx1 = x -> d_mx1 = a[l];
x -> mx2 = - INF; x -> cmx = 1;
return x;
}
int mid = (l + r) >> 1;
x -> ls = build(l, mid, a);
x -> rs = build(mid + 1, r, a);
x -> upd();
return x;
}

Node *root;

int n, m;
int a[N];

int main() {
std :: ios :: sync_with_stdio (false);
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
root = build(1, n, a);
while(m --) {
int opt;
cin >> opt;
if(opt == 1) {
int l, r, k;
cin >> l >> r >> k;
root -> modify1(l, r, k);
}
if(opt == 2) {
int l, r, k;
cin >> l >> r >> k;
root -> modify2(l, r, k);
}
if(opt == 3) {
int l, r;
cin >> l >> r;
cout << root -> query3(l, r) << endl;
}
if(opt == 4) {
int l, r;
cin >> l >> r;
cout << root -> query4(l, r) << endl;
}
if(opt == 5) {
int l, r;
cin >> l >> r;
cout << root -> query5(l, r) << endl;
}
}
return 0;
}

  • 如果功能不够, 现场使用矩阵实现即可。

左偏树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Node {
Node *ls, *rs;
int val, bh;
Node(int V, int B) {
val = V; ls = rs = NULL; bh = B;
}
};

Node* merge(Node *x, Node *y) {
if(! x) return y;
if(! y) return x;
if(x -> val > y -> val || (x -> val == y -> val && x -> bh > y -> bh))
swap(x, y);
x -> rs = merge(x -> rs, y);
if(rand() & 1) swap(x -> ls, x -> rs);
return x;
}

平衡树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 10;

int read() {
int x = 0, f = 1;
char a = getchar();
for(; ! isdigit(a); a = getchar()) if(a == '-') f = -1;
for(; isdigit(a); a = getchar()) x = x * 10 + a - '0';
return x * f;
}

int RandomSeed;

int rand() {
static const int Mod = 1e9 + 7;
return RandomSeed = ( 1LL * RandomSeed * 0x66CCFF % Mod + 19260817) % Mod;
}

int tot;
int ls[N], rs[N], val[N], rnd[N], sz[N];

void update(int x) {
sz[x] = sz[ls[x]] + sz[rs[x]] + 1;
}

int New(int k) {
val[++ tot] = k;
sz[tot] = 1;
rnd[tot] = rand();
return tot;
}

int merge(int x, int y) {
if(! x || ! y) return x + y;
if(rnd[x] <= rnd[y]) {
rs[x] = merge(rs[x], y);
update(x); return x;
}
else {
ls[y] = merge(x, ls[y]);
update(y); return y;
}
}

void split(int now, int k, int &x, int &y) {
if(! now) {
x = y = 0;
return ;
}
if(val[now] <= k) {
x = now;
split(rs[x], k, rs[x], y);
}
else {
y = now;
split(ls[y], k, x, ls[y]);
}
update(now);
}

int rt;

void ins(int k) {
int a, b;
split(rt, k, a, b);
rt = merge(a, merge(New(k), b));
}

void erase(int k) {
int a, b, c;
split(rt, k - 1, a, b);
split(b, k, b, c);
b = merge(ls[b], rs[b]);
rt = merge(a, merge(b, c));
}

int rnk(int k) {
int a, b;
split(rt, k - 1, a, b);
int ans = sz[a] + 1;
rt = merge(a, b);
return ans;
}

int Kth(int root, int k) {
int x = root;
while(x) {
if(sz[ls[x]] >= k) { x = ls[x]; continue; }
k -= sz[ls[x]] + 1;
if(k == 0) return x;
x = rs[x];
}
return -1;
}

int kth(int k) {
return val[Kth(rt, k)];
}

int pre(int k) {
int a, b;
split(rt, k - 1, a, b);
int ans = val[Kth(a, sz[a])];
rt = merge(a, b);
return ans;
}

int nxt(int k) {
int a, b;
split(rt, k, a, b);
int ans = val[Kth(b, 1)];
rt = merge(a, b);
return ans;
}

int main() {
int n = read();
while(n --) {
int op = read(), x = read();
if(op == 1) ins(x);
if(op == 2) erase(x);
if(op == 3) printf("%d\n", rnk(x));
if(op == 4) printf("%d\n", kth(x));
if(op == 5) printf("%d\n", pre(x));
if(op == 6) printf("%d\n", nxt(x));
}
return 0;
}

非常用, 存一个数组版本, 考试如果需要动态开点随便改改就可以了。

KDT

KDT没有什么封装的意义,因为平衡复杂度的操作往往需要访问内部点, 故直接贴板子。

来自洛谷 TATT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define I inline
#define ll long long
using namespace std;
#define int long long

template <typename T>
void read(T &x) {
x = 0; bool f = 0;
char c = getchar();
for (;!isdigit(c);c=getchar()) if (c=='-') f=1;
for (;isdigit(c);c=getchar()) x=x*10+(c^48);
if (f) x=-x;
}

const int N = 200500;
struct node {
int d[5], val, loc;
bool operator < (const node &k) const {
for (int i = 0;i < 5; i++)
if (d[i] != k.d[i]) return d[i] < k.d[i];
return 0;
}
}p[N];

int g[N], k;

I bool cmp(int a, int b) {
return p[a].d[k] < p[b].d[k];
}

#define ls son[x][0]
#define rs son[x][1]

int son[N][2];
int mx[N][4], mn[N][4], mxa[N], res[N], ans;
I void Mn(int &x, int y) { if (x > y) x = y; }
I void Mx(int &x, int y) { if (x < y) x = y; }

I void maintain(int x) {
for (int i = 0;i <= 3; i++) {
mx[x][i] = mn[x][i] = p[x].d[i+1];
if (ls) Mx(mx[x][i], mx[ls][i]), Mn(mn[x][i], mn[ls][i]);
if (rs) Mx(mx[x][i], mx[rs][i]), Mn(mn[x][i], mn[rs][i]);
}
}
int build(int l, int r, int d) {
if (l > r) return 0;
int mid = (l + r) >> 1;
k = d + 1, nth_element(g + l, g + mid, g + r + 1, cmp);
son[g[mid]][0] = build(l, mid - 1, (d + 1) % 4);
son[g[mid]][1] = build(mid + 1, r, (d + 1) % 4);
maintain(g[mid]); return g[mid];
}

int tmp;

// 判断x点是否在y点范围以内
inline bool in(int *x, int *y) {
int cnt = 0;
for (int i = 0;i < 4; i++) cnt += (x[i] <= y[i]);
return cnt == 4;
}

void query(int x, int y) {
if (mxa[x] <= tmp) return;
if (!in(mn[x], p[y].d + 1)) return;
if (in(mx[x], p[y].d + 1)) return tmp = mxa[x], void();
if (in(p[x].d + 1, p[y].d + 1)) Mx(tmp, res[x]);
if (ls) query(ls, y); if (rs) query(rs, y);
}

// 激活操作
void upit(int x, int y) {
if (x == y) {
res[x] = tmp, Mx(mxa[x], res[x]); return;
}
if (!in(p[y].d + 1, mx[x]) || !in(mn[x], p[y].d + 1)) return;
// 如果y点不在里面就返回
if (ls) upit(ls, y); if (rs) upit(rs, y);
Mx(mxa[x], mxa[ls]), Mx(mxa[x], mxa[rs]);
}

int rt, n;
int Ans[N];


void clear() {
rt = 0;
tmp = 0;
for (int i = 1; i <= n; i ++) g[i] = 0, son[i][0] = son[i][1] = 0, mxa[i] = 0, res[i] = 0;
ans = 0;
n = 0;
}

void solve() {
//freopen ("hs.in","r",stdin);

read(n);
for (int i = 1;i <= n; i++) {
read(p[i].d[0]), read(p[i].d[1]);
read(p[i].d[2]), read(p[i].d[3]);
//read(p[i].d[4]), read(p[i].val);
p[i].val = 1;
p[i].loc = i;
g[i] = i;
}
sort(p + 1, p + n + 1); rt = build(1, n, 0);
for (int i = 1;i <= n; i++)
tmp = 0, query(rt, i), tmp+=p[i].val, upit(rt, i), Mx(ans, tmp), Ans[p[i].loc] = tmp;
//for (int i = 1; i <= n; i ++) printf("%lld\n", Ans[i]);
printf("%lld\n", ans);
//cout << ans << endl;
clear();
}

signed main() {
int Case;
//read(Case);
Case = 1;
while (Case --) solve();
}

LCT

来自洛谷模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)

const int N = 1e5 + 5;

#define ls(x) ch[x][0]
#define rs(x) ch[x][1]

int val[N], sum[N], rev[N], ch[N][2], fa[N];
void upd(int x) {
sum[x] = sum[ls(x)] ^ sum[rs(x)] ^ val[x];
}
inline int nroot(int x) { return x == ls(fa[x]) || x == rs(fa[x]); }
inline int get(int x) { return x == rs(fa[x]); }
void rotate(int x) {
int y = fa[x], z = fa[y], k = get(x), w = ch[x][! k];
if (nroot(y)) ch[z][get(y)] = x; ch[x][! k] = y; ch[y][k] = w;
if (w) fa[w] = y; fa[y] = x; fa[x] = z; upd(y);
}
void reverse(int x) {
std :: swap(ls(x), rs(x)); rev[x] ^= 1;
}
void pushdown(int x) {
if (rev[x]) reverse(ls(x)), reverse(rs(x)), rev[x] = 0;
}
inline void pushall(int x) { if (nroot(x)) pushall(fa[x]); pushdown(x); }
void splay(int x) {
pushall(x);
while (nroot(x)) {
if (nroot(fa[x])) rotate(get(x) ? x : fa[x]);
rotate(x);
} upd(x);
}
void access(int x) {
for (int y = 0; x; x = fa[y = x])
splay(x), rs(x) = y, upd(x);
}
void makeroot(int x) {
access(x); splay(x); reverse(x);
}
void split(int x, int y) {
makeroot(x); access(y); splay(y);
}
int findroot(int x) {
access(x); splay(x);
while (ls(x)) x = ls(x);
return splay(x), x;
}
void link(int x, int y) {
makeroot(x);
if (findroot(y) != x) fa[x] = y;
}
void cut(int x, int y) {
makeroot(x);
if(findroot(y) == x && fa[y] == x && ls(y) == 0) {
makeroot(x);
fa[y] = rs(x) = 0;
upd(x);
}
}
int n, m;

int main() {
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
lep (i, 1, n) cin >> val[i], upd(i);
while (m --) {
int opt, x, y;
cin >> opt >> x >> y;
if (opt == 0) { split(x, y); cout << sum[y] << endl; }
if (opt == 1) link(x, y);
if (opt == 2) cut(x, y);
if (opt == 3) { splay(x); val[x] = y; upd(x); }
}
return 0;
}

李超树

支持合并的李超树模板, 动态开点, 支持查询某个地方最小值, 每次全局加线段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct Line {
LL k, b;
Line(LL K = 0, LL B = 0) : k(K), b(B) {}
LL calc(LL x) const { return k * x + b; }
double inter(const Line &a) const {
return (double) (a.b - b) / (k - a.k);
}
} ;

Line tr[N << 5];
int ls[N << 5], rs[N << 5];
int tot;

void insert(int &x, int l, int r, const Line &lns) {
if(! x) {
x = ++ tot;
tr[x] = lns;
return ;
}
if(tr[x].calc(l) > lns.calc(l) && tr[x].calc(r) > lns.calc(r)) {
tr[x] = lns;
return ;
}
if(tr[x].calc(l) < lns.calc(l) && tr[x].calc(r) < lns.calc(r)) {
return ;
}
double p = tr[x].inter(lns);
int mid = (l + r) >> 1;
if(tr[x].k < lns.k) {
if(mid < p)
insert(rs[x], mid + 1, r, tr[x]),
tr[x] = lns;
else insert(ls[x], l, mid, lns);
}
else {
if(mid > p)
insert(ls[x], l, mid, tr[x]),
tr[x] = lns;
else
insert(rs[x], mid + 1, r, lns);
}
}

int merge(int x, int y, int l, int r) {
if(! x || ! y) return x + y;
if(l == r) {
if(tr[x].calc(l) > tr[y].calc(l))
tr[x] = tr[y];
return x;
}
int mid = (l + r) >> 1;
ls[x] = merge(ls[x], ls[y], l, mid);
rs[x] = merge(rs[x], rs[y], mid + 1, r);
insert(x, l, r, tr[y]);
return x;
}

LL query(int x, int l, int r, int v) {
if(! x) return 1e18;
LL res = tr[x].calc(v);
if(l == r) return res;
int mid = (l + r) >> 1;
if(v <= mid) return min(res, query(ls[x], l, mid, v));
else return min(res, query(rs[x], mid + 1, r, v));
}

树链剖分

返回所有需要修改或者查询的区间, 传入大小, 根节点和存边的vector, 如果需要排序直接按 dfn sort 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#include <bits/stdc++.h>

#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)
#define debug(...) fprintf (stderr, __VA_ARGS__)

using std :: cin;
using std :: cout;
using std :: endl;
using std :: cerr;
using i64 = long long;

int P;

struct HLD {
std :: vector<int> fa, dep, dfn, sz, top, son, loc;
int n, rt;
std :: vector<std :: vector<int> > e;

void dfs1 (int x, int fx) {
sz[x] = 1; dep[x] = dep[fx] + 1; fa[x] = fx;
for (int y : e[x]) if (y != fx) {
dfs1 (y, x), sz[x] += sz[y];
if (sz[y] >= sz[son[x]]) son[x] = y;
}
}

void dfs2 (int x, int topx) {
top[x] = topx;
dfn[x] = ++ dfn[0];
if (son[x]) dfs2 (son[x], topx);
for (int y : e[x]) if (y != fa[x] && y != son[x]) dfs2 (y, y);
}

void init (int _n, int _rt, std :: vector<std :: pair<int, int> > edge) {
n = _n; rt = _rt;
fa.resize (n + 1);
dep.resize (n + 1);
dfn.resize (n + 1);
sz.resize (n + 1);
top.resize (n + 1);
son.resize (n + 1);
loc.resize (n + 1);
e.resize (n + 1);
for (auto [x, y] : edge) e[x].push_back (y), e[y].push_back (x);
dfs1 (rt, 0);
dfs2 (rt, rt);
lep (i, 1, n) loc[dfn[i]] = i;
}

std :: pair<int, int> subtree (int x) {
return { dfn[x], dfn[x] + sz[x] - 1 };
}

std :: vector<std :: pair<int, int> > chain (int x, int y) {
std :: vector<std :: pair<int, int> > rec;
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) std :: swap (x, y);
rec.push_back ( {dfn[top[x]], dfn[x] } );
x = fa[top[x]];
}
if (dep[x] > dep[y]) std :: swap (x, y);
rec.push_back ( {dfn[x], dfn[y]} );
return rec;
}
} hld;

const int N = 2e5 + 5;

int n, m, rt;
int val[N];

#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)

i64 tr[N << 2], tg[N << 2], len[N << 2];

void build(int x, int l, int r) {
len[x] = r - l + 1;
if(l == r) { tr[x] = val[hld.loc[l]]; return ; }
int mid = (l + r) >> 1;
build(ls(x), l, mid);
build(rs(x), mid + 1, r);
tr[x] = (tr[ls(x)] + tr[rs(x)]) % P;
}

inline void down(int x, i64 v) {
tr[x] += v * len[x]; tg[x] += v;
tr[x] %= P; tg[x] %= P;
}

inline void pushdown(int x) {
if(tg[x]) {
down(ls(x), tg[x]);
down(rs(x), tg[x]);
tg[x] = 0;
}
}

inline void modify(int x, int l, int r, int L, int R, i64 v) {
if(L <= l && r <= R) {
down(x, v); return ;
}
pushdown(x);
int mid = (l + r) >> 1;
if(L <= mid) modify(ls(x), l, mid, L, R, v);
if(R > mid) modify(rs(x), mid + 1, r, L, R, v);
tr[x] = (tr[ls(x)] + tr[rs(x)]) % P;
}

i64 query(int x, int l, int r, int L, int R) {
if(L <= l && r <= R) return tr[x];
pushdown(x);
int mid = (l + r) >> 1;
i64 ans = 0;
if(L <= mid) ans += query(ls(x), l, mid, L, R);
if(R > mid) ans += query(rs(x), mid + 1, r, L, R);
return ans % P;
}

int main() {
std :: ios :: sync_with_stdio(false);
cin >> n >> m >> rt >> P;
lep (i, 1, n) cin >> val[i];
std :: vector<std :: pair<int, int> > edge;
lep (i, 2, n) {
int x, y;
cin >> x >> y;
edge.push_back ( {x, y} );
}
hld.init (n, rt, edge);
build (1, 1, n);
while (m --) {
int op;
cin >> op;
if (op == 1) {
int x, y, z;
cin >> x >> y >> z;
auto vec = hld.chain (x, y);
for (auto [l, r] : vec) modify (1, 1, n, l, r, z);
}
if (op == 2) {
int x, y, ans = 0;
cin >> x >> y;
auto vec = hld.chain (x, y);
for (auto [l, r] : vec) ans += query (1, 1, n, l, r), ans %= P;
cout << ans << endl;
}
if (op == 3) {
int x, z;
cin >> x >> z;
auto [l, r] = hld.subtree (x);
modify (1, 1, n, l, r, z);
}
if (op == 4) {
int x;
cin >> x;
auto [l, r] = hld.subtree (x);
cout << query (1, 1, n, l, r) % P << endl;
}
}
return 0;
}

LCA

ST表求LCA

一倍的 log 空间的lca求法。

$O(n \log n) - O(1)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct LCA {
std :: vector<std :: vector<int> > e, st;
std :: vector<int> dep, dfn;
int tot;

void dfs1(int x, int fx) {
dep[x] = dep[fx] + 1;
dfn[x] = ++ tot;
st[0][tot] = fx;
for (int y : e[x]) if (y != fx) dfs1(y, x);
}

inline int upd(int x, int y) { return dep[x] < dep[y] ? x : y; }

inline int lca(int x, int y) {
if (x == y) return x;
x = dfn[x]; y = dfn[y];
if (x > y) std :: swap(x, y);
int d = log2 (y - x ++);
return upd(st[d][x], st[d][y - (1 << d) + 1]);
}

void init (int n, int rt, std :: vector<std :: pair<int, int> > edge) {
e.resize (n + 1); int lg = log2 (n) + 1; tot = 0;
dep.resize (n + 1), dfn.resize (n + 1);
st.resize (lg + 1); for (int i = 0; i <= lg; i ++) st[i].resize (n + 1);
for (auto [x, y] : edge) e[x].push_back (y), e[y].push_back (x);
dfs1 (rt, 0);
for (int i = 1; i <= lg; i ++)
for (int j = 1; j + (1 << i) - 1 <= n; j ++)
st[i][j] = upd(st[i - 1][j], st[i - 1][j + (1 << (i - 1))]);
}
} ;

虚树

考虑到边权问题不进行封装, 使用的时候把lca复制过来直接跑这个就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int dfn[maxn];
bool valid[maxn];
int h[maxn], m, a[maxn], len; // 存储关键点

bool cmp(int x, int y) {
return dfn[x] < dfn[y]; // 按照 dfn 序排序
}

void build_virtual_tree() {
sort(h + 1, h + m + 1, cmp); // 把关键点按照 dfn 序排序
for (int i = 1; i < m; ++i) {
a[++len] = h[i];
a[++len] = lca(h[i], h[i + 1]); // 插入 lca
}
a[++len] = h[m];
sort(a + 1, a + len + 1, cmp); // 把所有虚树上的点按照 dfn 序排序
len = unique(a + 1, a + len + 1) - a - 1; // 去重
for (int i = 1, lc; i < len; ++i) {
lc = lca(a[i], a[i + 1]);
conn(lc, a[i + 1]); // 连边,如有边权 就是 distance(lc,a[i+1])
}
}

图论

最短路

dijskra 堆优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
std :: vector<std :: pair<int, int> > e[N];

i64 dis[N];
int vis[N];

void dijskra() {
std :: priority_queue<std :: pair<int, int> > q;
memset(dis, 0x3f, sizeof(dis));
q.push( {0, s} ); dis[s] = 0;
while (q.size()) {
int x = q.top().second; q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (auto p : e[x]) {
int y, w;
std :: tie(y, w) = p;
if (dis[y] > dis[x] + w) {
dis[y] = dis[x] + w;
q.push( {- dis[y], y} );
}
}
}
}

暴力实现 dijskra 每次暴力找 dis 最小的点即可。

Johnson 全源最短路

考虑解决负权边问题

构造点权 $val_i$,边权 $w_{u,v}$ 加上 $val_u-val_v$

则此时 $dis_T=realdis_T+val_S-val_T$

根据 $dis_v\leq dis_u+w_{u,v}$,则 $w_{u,v}+dis_u-dis_v\geq0$,所以 $val_i=dis_i$($dis_i$ 可由一遍 SPFA 求出)

欧拉回路/欧拉路

有向图字典序最小欧拉路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const int N = 4e5 + 10;

int n, m;
int in[N], out[N];

struct edge {
int to, next;
} e[N << 1];

int cnt, head[N];

void push(int x, int y) {
e[++ cnt] = (edge) {y, head[x]}; head[x] = cnt;
}

int top, stk[N];

void dfs(int x) {
for(int i = head[x]; i; i = head[x]) {
int y = e[i].to; head[x] = e[i].next;
dfs(y);
}
stk[++ top] = x;
}

int main() {
read(n); read(m);
vector<pair<int, int> > vec;
lep (i, 1, m) {
int x, y;
read(x); read(y);
in[y] ++;
out[x] ++;
vec.pb({x, y});
}
sort(vec.begin(), vec.end(), [&] (auto a, auto b) { return a.fi != b.fi ? a.fi < b.fi : a.se > b.se;});
for(auto p : vec) push(p.fi, p.se);

int u = 1, pu = 0;
lep (i, 1, n) if(abs(in[i] - out[i]) > 1) return puts("No"), 0;
lep (i, 1, n) if(out[i] - in[i] == 1) u = i, pu --; else if(in[i] - out[i] == 1) pu ++;
if(pu == 0) {
dfs(u);
rep (i, top, 1) printf("%d ", stk[i]);
}
else puts("No");
return 0;
}

欧拉回路

Case = 1 无向图, Case = 1有向图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
const int N = 1e5 + 10;

struct edge {
int to, next, w;
} e[N << 2];
int cnt = 1, head[N];

void add(int x, int y, int w) {
e[++ cnt] = (edge) {y, head[x], w}; head[x] = cnt;
}

int n, m, Case;
int in[N], out[N];
int stk[N << 2], top, vis[N << 2];

void dfs(int x) {
for(int &i = head[x]; i; i = e[i].next) {
int y = e[i].to; if(vis[i]) continue;
if(Case == 1) vis[i] = vis[i ^ 1] = 1;
else vis[i] = 1;
int w = e[i].w; dfs(y);
stk[++ top] = w;
}
}

int main() {
//freopen("a.in", "r", stdin);
//freopen("a.out", "w", stdout);
Case = read();
n = read(); m = read();
for(int i = 1; i <= m; i ++) {
int v = read(), u = read();
if(Case == 1) {
in[v] ++; in[u] ++;
add(v, u, i); add(u, v, - i);
}
else {
in[u] ++; out[v] ++;
add(v, u, i);
}
}
if(Case == 1) {
for(int i = 1; i <= n; i ++)
if(in[i] & 1) {
printf("NO\n"); return 0;
}
}
else {
for(int i = 1; i <= n; i ++)
if(in[i] != out[i]) {
printf("NO\n"); return 0;
}
}
for(int i = 1; i <= n; i ++)
if(in[i] || out[i]) {
dfs(i);
break;
}
if(top != m) {
printf("NO\n"); return 0;
}
printf("YES\n");
for(int i = top; i >= 1; i --) printf("%d ", stk[i]);
return 0;
}

K短路 (可并堆优化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <bits/stdc++.h>

using namespace std;

#define LL long long

const int N = 5e3 + 5;
const int M = 2e5 + 5;
const double inf = 1e15;

int n, m;
double E;

struct Graph {
int head[N], nxt[M], to[M]; double w[M];
int cnt;
void push(int x, int y, double W) {
to[++ cnt] = y; w[cnt] = W; nxt[cnt] = head[x]; head[x] = cnt;
}
} G[2];
Graph *G1 = &G[0], *G2 = &G[1];

double dis[N];
int vis[N], pre[N];

void dijskra() {
static priority_queue<pair<double, int> > q;
for(int i = 1; i <= n; i ++) dis[i] = inf, vis[i] = 0;
q.push(make_pair(0, n)); dis[n] = 0;
while(q.size()) {
int x = q.top().second; q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(int i = G2 -> head[x]; i; i = G2 -> nxt[i]) {
int y = G2 -> to[i];
if(dis[y] > dis[x] + G2 -> w[i]) {
dis[y] = dis[x] + G2 -> w[i];
q.push(make_pair(- dis[y], y));
pre[y] = i;
}
}
}
}

#define ls(x) t[x].ch[0]
#define rs(x) t[x].ch[1]
#define val(x) t[x].val

struct Node {
int ch[2], dist;
int to; double val;
}t[M << 5];
int tot;

inline int New(int to, double val) {
tot ++;
t[tot] = (Node) {0, 0, 1, to, val};
return tot;
}

int merge(int x, int y) {
if(! x || ! y) return x | y;
int z = ++ tot;
if(val(x) > val(y)) swap(x, y);
t[z] = t[x];
rs(z) = merge(rs(z), y);
if(t[ls(z)].dist < t[rs(z)].dist) swap(ls(z), rs(z));
t[z].dist = t[rs(z)].dist + 1;
return z;
}

int rt[N];
int pos[N];

inline bool cmp(int x, int y) {
return dis[x] < dis[y];
}

int main() {
ios :: sync_with_stdio(false);
cin >> n >> m >> E;
for(int i = 1; i <= m; i ++) {
int x, y; double w;
cin >> x >> y >> w;
if(x == n) {
m --; i --; continue;
}
G1 -> push(x, y, w);
G2 -> push(y, x, w);
}
dijskra();
for(int x = 1; x <= n; x ++)
for(int i = G1 -> head[x]; i; i = G1 -> nxt[i]) {
if(i == pre[x]) continue;
int y = G1 -> to[i]; // orz tyx
rt[x] = merge(rt[x], New(y, - dis[x] + dis[y] + G1 -> w[i]));
}
for(int i = 1; i <= n; i ++) pos[i] = i;
sort(pos + 1, pos + 1 + n, cmp);
for(int i = 2; i <= n; i ++) {
int x = pos[i];
rt[x] = merge(rt[x], rt[G1 -> to[pre[x]]]);
}
static priority_queue<pair<double, int> >q;
int ans = 0;
if(E < dis[1]) { cout << 0 << endl; return 0; }
E -= dis[1]; ans ++;
if(rt[1])
q.push(make_pair(- dis[1] - val(rt[1]), rt[1]));
while(q.size()) {
int x = q.top().second; double nwval = - q.top().first;
q.pop();
if(E < nwval) { cout << ans << endl; return 0; }
E -= nwval; ans ++;
if(ls(x)) q.push(make_pair(- nwval + val(x) - val(ls(x)), ls(x)));
if(rs(x)) q.push(make_pair(- nwval + val(x) - val(rs(x)), rs(x)));
if(rt[t[x].to]) q.push(make_pair(- nwval - val(rt[t[x].to]), rt[t[x].to]));
}
cout << ans << endl;
return 0;
}

连通分量

点双

给出所有割点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int n, m;
struct edge {
int to, next;
} e[M];

int cnt, head[N];
void add(int x, int y) {
e[++ cnt] = (edge) {y, head[x]}; head[x] = cnt;
}

int dfn[N], low[N], bj[N], num;
int rt;

void tarjan(int x, int fx) {
dfn[x] = low[x] = ++ num;
int sz = 0;
for(int i = head[x]; i; i = e[i].next) {
int y = e[i].to;
if(y == fx) continue;
if(! dfn[y]) {
tarjan(y, x), low[x] = min(low[x], low[y]), sz ++;
if(low[y] >= dfn[x] && x != rt) bj[x] = 1;
}
else {
low[x] = min(low[x], dfn[y]);
}
}
if(x == rt && sz >= 2) bj[x] = 1;
}

int main() {
//freopen("a.in", "r", stdin);
//freopen("a.out", "w", stdout);
n = read(); m = read();
for(int i = 1; i <= m; i ++) {
int x = read(), y = read();
add(x, y); add(y, x);
}
for(int i = 1; i <= n; i ++)
if(! dfn[i]) rt = i, tarjan(i, 0);
vector<int> ans;
for(int i = 1; i <= n; i ++)
if(bj[i]) ans.push_back(i);
print(ans.size());
for(int i : ans) print(i, ' ');
return 0;
}

边双

给出所有割边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int low[MAXN], dfn[MAXN], dfs_clock;
bool isbridge[MAXN];
vector<int> G[MAXN];
int cnt_bridge;
int father[MAXN];

void tarjan(int u, int fa) {
father[u] = fa;
low[u] = dfn[u] = ++dfs_clock;
for (int i = 0; i < G[u].size(); i++) {
int v = G[u][i];
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
isbridge[v] = true;
++cnt_bridge;
}
} else if (dfn[v] < dfn[u] && v != fa) {
low[u] = min(low[u], dfn[v]);
}
}
}

强连通分量

tarjan

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int n, m;
vector<int> e[N];
vector<pair<int, int> >edge;
int a[N];

int dfn[N], low[N], co[N], col, sum[N];
int stk[N], top;

void tarjan(int x) {
stk[++ top] = x;
dfn[x] = low[x] = ++ dfn[0];
for(int y : e[x]) {
if(! dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if(! co[y]) low[x] = min(low[x], dfn[y]);
}
if(low[x] == dfn[x]) {
co[x] = ++ col;
sum[col] = a[x];
while(stk[top] != x) {
co[stk[top]] = col;
sum[col] += a[stk[top]];
top --;
}
top --;
}
}

kosoraju

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// g 是原图,g2 是反图

void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v]) dfs1(v);
s.push_back(u);
}

void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v]) dfs2(v);
}

void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i]) dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}

最小割树

给定一个 $n$ 个点 $m$ 条边的无向连通图,多次询问两点之间的最小割

$n = 500, m = 1500, Q = 10^5$ 复杂度很高但是很快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)

const int N = 500 + 5;
const int M = 3000 + 5;
const int INF = 0x3f3f3f3f;

int n, m;
struct Node_Edge { int x, y, w; } ;
vector<Node_Edge> edge;

struct Tree {
vector<pair<int, int> > e[N];
void push(int x, int y, int w) {
e[x].push_back( {y, w} );
e[y].push_back( {x, w} );
}
int lg[N], dep[N];
int fa[N][20], jp[N][20];
void dfs(int x, int fx, int fw) {
fa[x][0] = fx;
jp[x][0] = fw;
dep[x] = dep[fx] + 1;
lep (i, 1, lg[dep[x]]) fa[x][i] = fa[fa[x][i - 1]][i - 1], jp[x][i] = min(jp[x][i - 1], jp[fa[x][i - 1]][i - 1]);
for(auto p : e[x]) if(p.first != fx) dfs(p.first, x, p.second);
}
int qry(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
int ans = INF;
while(dep[x] > dep[y]) {
int i = lg[dep[x] - dep[y]];
ans = min(ans, jp[x][i]);
x = fa[x][i];
}
if(x == y) return ans;
rep (i, lg[dep[x]], 0) if(fa[x][i] ^ fa[y][i]) {
ans = min(ans, jp[x][i]);
x = fa[x][i];
ans = min(ans, jp[y][i]);
y = fa[y][i];
}
ans = min(ans, jp[x][0]);
ans = min(ans, jp[y][0]);
return ans;
}
void build() {
lep (i, 2, n) lg[i] = lg[i >> 1] + 1;
dfs(1, 0, 0);
}
} T;

struct Flow_Solution {
struct edge {
int to, next, f;
} e[M << 1];
int cnt;
int cur[N], head[N], dep[N];
void push(int x, int y, int f) {
e[++ cnt] = (edge) {y, head[x], f}; head[x] = cnt;
e[++ cnt] = (edge) {x, head[y], 0}; head[y] = cnt;
}
int S, T;
bool bfs() {
queue<int> q;
lep (x, 0, n) dep[x] = INF, cur[x] = head[x];
q.push(S); dep[S] = 0;
while(q.size()) {
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].next) {
if(e[i].f && dep[e[i].to] == INF) {
dep[e[i].to] = dep[x] + 1;
q.push(e[i].to);
}
}
}
return dep[T] != INF;
}
int dfs(int x, int lim) {
if(x == T || lim == 0) return lim;
int flow = 0, tmp;
for(int &i = cur[x]; i; i = e[i].next)
if(dep[e[i].to] == dep[x] + 1 && (tmp = dfs(e[i].to, min(lim, e[i].f)))) {
e[i].f -= tmp;
e[i ^ 1].f += tmp;
flow += tmp;
lim -= tmp;
if(lim == 0) break;
}
return flow;
}
int solve(int _S, int _T, vector<int> &node) {
cnt = 1; S = _S; T = _T;
lep (i, 0, n) head[i] = 0;
memset(head, 0, sizeof(head));
for(auto p : :: edge) push(p.x, p.y, p.w), push(p.y, p.x, p.w);
int flow = 0;
while(bfs()) flow += dfs(S, INF);
return flow;
}
} Flow;

void solve(vector<int> &node) {
if(node.size() <= 1) return ;
int SS = node[0];
int TT = node[1];
int flow = Flow.solve(SS, TT, node);
T.push(SS, TT, flow);
vector<int> lnode, rnode;
vector<Node_Edge> ledge, redge;
for(auto x : node)
if(Flow.dep[x] != INF) lnode.push_back(x);
else rnode.push_back(x);
solve(lnode);
solve(rnode);
}

int main() {
vector<int> node;
n = read(); m = read();
lep (i, 1, m) {
int x = read(), y = read(), w = read();
edge.push_back( {x, y, w} );
}
lep (i, 1, n) node.push_back(i);
solve(node);
T.build();
int q = read();
while(q --) {
int x = read(), y = read();
printf("%d\n", T.qry(x, y));
}
return 0;
}

斯坦纳树

花费最小代价连通给定的 K 个点。
复杂度 $O (n \times 3^k + m \log m \times 2^k)$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'

using i64 = long long;

const int N = 100 + 5;
const int M = 500 + 5;
const int S = 10;

int n, m, k;
std :: vector<std :: pair<int, int> > e[N];
int f[N][1 << 10];

std :: priority_queue<std :: pair<int, int>> q;


int main() {
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> k;
for(int i = 1, x, y, w; i <= m; i ++) {
cin >> x >> y >> w;
e[x].push_back( {y, w} );
e[y].push_back( {x, w} );
}
memset(f, 0x3f, sizeof(f));
for(int i = 1; i <= k; i ++) {
int x;
cin >> x;
f[x][(1 << (i - 1))] = 0;
}

auto dijksra = [&] (int s) -> void {
static int vis[N];
memset(vis, 0, sizeof(vis));
while(q.size()) {
int x = q.top().second; q.pop();
for(auto p : e[x]) {
int y, w;
std :: tie(y, w) = p;
if(f[y][s] > f[x][s] + w) {
f[y][s] = f[x][s] + w;
q.push( {- f[y][s], y} );
}
}
}
} ;

for(int s = 0; s < (1 << k); s ++) {
for(int i = 1; i <= n; i ++) {
for(int t = s & (s - 1); t; t = s & (t - 1))
f[i][s] = std :: min(f[i][s], f[i][t] + f[i][s ^ t]);
if(f[i][s] != 0x3f3f3f3f) q.push( {- f[i][s], i} );
}
dijksra(s);
}

int ans = 0x3f3f3f3f;
for(int i = 1; i <= n; i ++) ans = std :: min(ans, f[i][(1 << k) - 1]);
cout << ans << endl;

return 0;
}

竞赛图相关

竞赛图哈密顿回路

A Blog

在求回路之前, 先求哈密顿路。

竞赛图哈密顿路

考虑增量法, 假如当前已经有了路径$v_1\rightarrow v_2 \rightarrow \cdots \rightarrow v_3 \rightarrow v_k$, 考虑增加一个点 $v_{k + 1}$ 。

  • 若存在$v_k \rightarrow v_{k + 1}$, 那么直接把 $v_{k + 1}$ 接在 $v_k$ 后面即可。
  • 若存在$v_{k + 1} \rightarrow v_1$, 那么直接把$v_{k + 1}$接在前面即可。
  • 否则从前往后找一个点$v_i$, 使得存在边$v_{k + 1} \rightarrow v_{i + 1}$, 然后把$v_{k + 1}$放在$v_i$后面即可。

竞赛图哈密顿回路

定理:竞赛图的任意强连通子图必存在哈密顿回路。

  • 枚举起点,求哈密顿通路,判断是否首尾相连

上面这个虽然不知道为什么是对的, 但是很好记对吧, 不记得下面这个了就用上面那个, 反正常数应该不大

  • 找到第一个能连回1号点的点, 设其为$L$, $1$号点为$R$, 得到了一个环, 现在扩充这个环, 使其包含所有点。
  • 从$L$往后枚举每个点$i$,表示现在要把点$i$加入环中.
  • 从$R$开始枚举已求出的环上的每个点,找到第一个存在边$i\rightarrow j$ 的点$j$.
  • 如果找不到这样的点$j$,继续枚举$i$的下一个点。

虽然并不知道为啥对

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
inline void paint(int ID) {
vector<int> &vec = blk[ID];
vector<int> &ans = road[ID];
static int nxt[N], l, r;
memset(nxt, 0, sizeof(nxt));
l = r = vec[0];
lep (p, 1, vec.size() - 1) {
int x = vec[p];
if(e[x][l]) nxt[x] = l, l = x;
else if(e[r][x]) nxt[r] = x, r = x;
else {
for(int u = l; ; u = nxt[u]) if(e[x][nxt[u]]) {
nxt[x] = nxt[u];
nxt[u] = x; break;
}
}
}
r = 0;
for(int x = l; x; x = nxt[x]) if(r) {
for(int u = r, v = l; ; u = v, v = nxt[v]) {
if(e[x][v]) {
nxt[u] = nxt[r];
if(u != r) nxt[r] = l;
l = v; r = x; break;
}
if(v == r) break;
}
}
else if(e[x][l]) r = x;
int x = l;
while(1) {
if(x != 0) ans.push_back(x);
if(x == r) break;
x = nxt[x];
}
}

支配树

返回每个点在支配树上的父亲。

单 log 复杂度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
namespace DominateTree {
const int N = 5e5 + 10;
const int M = 5e5 + 10;
int n, m;
struct edge {
int to, next;
} e[M * 3];

int head[3][N], cnt;

void Ins(int id, int x, int y) {
e[++ cnt] = (edge) {y, head[id][x]}; head[id][x] = cnt;
}

int dfn[N], stk[N], co;
int fa[N];

void Tarjan(int x) {
stk[dfn[x] = ++ co] = x;
for(int i = head[0][x]; i; i = e[i].next) {
int y = e[i].to;
if(! dfn[y]) {
fa[y] = x;
Tarjan(y);
}
}
}

int idom[N], sdom[N];
int Fa[N], mn[N];

int Find(int x) {
if(x == Fa[x]) return x;
int res = Find(Fa[x]);
if(dfn[sdom[mn[Fa[x]]]] < dfn[sdom[mn[x]]]) mn[x] = mn[Fa[x]];
return Fa[x] = res;
}

void Contract(int st) {
Tarjan(st);
for(int i = 1; i <= n; i ++) sdom[i] = Fa[i] = mn[i] = i;
for(int i = co; i >= 2; i --) {
int x = stk[i];
for(int i = head[1][x]; i; i = e[i].next) {
int y = e[i].to;
if(! dfn[y]) continue;
Find(y);
if(dfn[sdom[mn[y]]] < dfn[sdom[x]])
sdom[x] = sdom[mn[y]];
}
Fa[x] = fa[x];
Ins(2, sdom[x], x);
for(int i = head[2][x = fa[x]]; i; i = e[i].next) {
int y = e[i].to;
Find(y);
idom[y] = x == sdom[mn[y]] ? x : mn[y];
}
head[2][x] = 0;
}
for(int i = 2; i <= co; i ++) {
int x = stk[i];
if(idom[x] ^ sdom[x]) idom[x] = idom[idom[x]];
}
}

void clear() {
cnt = 0;
lep (i, 0, 2) lep (j, 1, n) head[i][j] = 0;
lep (i, 1, n) dfn[i] = stk[i] = fa[i] = idom[i] = sdom[i] = Fa[i] = mn[i] = 0;
co = 0;
}

std :: vector<int> solve(int _n, int _m, std :: vector<std :: pair<int, int> > &edge) {
n = _n;
m = _m;
for (auto &[x, y] : edge) Ins(0, x, y), Ins(1, y, x);
Contract(1);
std :: vector<int> fa(n + 1);
for (int i = 1; i <= n; i ++) fa[i] = idom[i];
clear();
return fa;
}
}

2-SAT

连边条件就是如果不满足某个某个条件就向必要的那个条件连边。

比如 “如果是a是0, 则b是1, 那么a0 -> b1 连边”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const int N = 2e6 + 10;

int n, m;
vector<int> e[N];

int dfn[N], low[N], stk[N], top, co[N], col;

void tarjan(int x) {
dfn[x] = low[x] = ++ dfn[0];
stk[++ top] = x;
for (int y : e[x]) {
if(! dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if(! co[y]) low[x] = min(low[x], dfn[y]);
}
if(low[x] == dfn[x]) {
co[x] = ++ col;
while(stk[top] != x) {
co[stk[top]] = col;
top --;
}
top --;
}
}

int main() {
read(n); read(m);
lep (o, 1, m) {
int i, a, j, b;
read(i); read(a); read(j); read(b);
if(a == 1 && b == 1)
e[i + n].pb(j), e[j + n].pb(i);
if(a == 0 && b == 1)
e[i].pb(j), e[j + n].pb(i + n);
if(a == 1 && b == 0)
e[j].pb(i), e[i + n].pb(j + n);
if(a == 0 && b == 0)
e[i].pb(j + n), e[j].pb(i + n);
}
lep (i, 1, n * 2) if(! dfn[i]) tarjan(i);
lep (i, 1, n) if(co[i] == co[i + n]) printf("IMPOSSIBLE\n"), exit(0);
printf("POSSIBLE\n");
lep (i, 1, n) printf("%d%c", co[i] < co[i + n], " \n"[i == n]);
return 0;
}

差分约束

直接复制最短路。

网络流

最大流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using i64 = long long;

const int N = 5e3 + 5;

int n, m, s, t;
struct edge {
int to, next, f;
} e[N << 1];

int head[N];
int cnt = 1;
inline void _push(int x, int y, int f) {
e[++ cnt] = (edge) {y, head[x], f}; head[x] = cnt;
}
inline void push(int x, int y, int f) {
_push(x, y, f); _push(y, x, 0);
}

const int INF = 0x3f3f3f3f;

int dep[N], cur[N];

int bfs() {
queue<int> q; q.push(s);
lep (i, 1, n) cur[i] = head[i], dep[i] = INF;
dep[s] = 0;
while(q.size()) {
int x = q.front(); q.pop();
for(int i = head[x]; i; i = e[i].next) if(e[i].f && dep[e[i].to] == INF) {
dep[e[i].to] = dep[x] + 1;
q.push(e[i].to);
}
}
return dep[t] != INF;
}

int dfs(int x, int lim) {
if(x == t || lim == 0) return lim;
int flow = 0, tmp;
for(int &i = cur[x]; i; i = e[i].next) if(dep[e[i].to] == dep[x] + 1 && (tmp = dfs(e[i].to, min(lim, e[i].f)))) {
flow += tmp;
e[i].f -= tmp;
e[i ^ 1].f += tmp;
lim -= tmp;
if(lim == 0) break;
}
return flow;
}

int main() {
ios :: sync_with_stdio(false);
cin >> n >> m >> s >> t;
for(int i = 1, u, v, f; i <= m; i ++) {
cin >> u >> v >> f;
push(u, v, f);
}
long long ans = 0;
while(bfs()) ans += dfs(s, INF);
cout << ans << endl;
return 0;
}

最小费用最大流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
struct Net {
struct edge {
int to, next, f, w;
} e[M << 1];
int cnt, head[N];
void Add(int x, int y, int f, int w) {
e[++ cnt] = (edge) {y, head[x], f, w}; head[x] = cnt;
}
void add(int x, int y, int f, int w) {
Add(x, y, f, w); Add(y, x, 0, -w);
}
int S, T;
int flow, cost;
int vis[N], dis[N], cur[N];
bool spfa() {
queue<int> q;
memset(vis, 0, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
memcpy(cur, head, sizeof(cur));
q.push(S); dis[S] = 0;
while(q.size()) {
int x = q.front(); q.pop();
vis[x] = 0;
for(int i = head[x]; i; i = e[i].next) {
int y = e[i].to;
if(dis[y] > dis[x] + e[i].w && e[i].f) {
dis[y] = dis[x] + e[i].w;
if(! vis[y]) { vis[y] = 1; q.push(y); }
}
}
}
return dis[T] != 0x3f3f3f3f;
}
int dfs(int x, int f) {
if(x == T) {
flow += f;
cost += f * dis[T];
return f;
}
vis[x] = 1;
int res = 0, tmp;
for(int &i = cur[x]; i; i = e[i].next) {
int y = e[i].to;
if(vis[y]) continue;
if(e[i].f && dis[y] == dis[x] + e[i].w) {
tmp = dfs(y, min(f - res, e[i].f));
e[i].f -= tmp;
e[i ^ 1].f += tmp;
res += tmp;
if(res == f) break;
}
}
return res;
}
void MCMF() {
while(spfa()) dfs(S, 0x3f3f3f3f);
cout << flow << ' ' << cost << endl;
}
Net() {
cnt = 1;
}
} net;

无源汇有上下界可行流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
int n, m;
struct edge {
int to, next, f;
} e[M * 2 + N * 2];
int low[M], upp[M];
int cnt = 1, head[N];
inline void add(int x, int y, int z) {
e[++ cnt] = {y, head[x], z};
head[x] = cnt;
}
int U[M], V[M], LOW[M], UPP[M];
int du[N];
int S, T;
int dep[N], cur[N];
queue<int> q;
inline int bfs() {
memset(dep, inf, sizeof(dep));

for (R int i = 1; i <= T; i ++)
cur[i] = head[i];

dep[S] = 0;
q.push(S);

while (q.size()) {
int x = q.front();
q.pop();

for (R int i = head[x]; i; i = e[i].next) {
if (e[i].f == 0)
continue;

int y = e[i].to;

if (dep[y] < inf)
continue;

dep[y] = dep[x] + 1;
q.push(y);
}
}

return dep[T] < inf;
}
inline int dfs(int x, int lim) {
if (lim == 0)
return 0;

if (x == T)
return lim;

int res = 0;

for (R int i = cur[x]; i; i = e[i].next) {
int y = e[i].to, tmp;
cur[x] = i;

if (dep[y] == dep[x] + 1 && (tmp = dfs(y, min(lim, e[i].f)))) {
res += tmp;
lim -= tmp;
e[i].f -= tmp;
e[i ^ 1].f += tmp;

if (tmp == 0)
break;
}
}

return res;
}
int main() {
//freopen("2.in", "r", stdin);
//freopen("a.out", "w", stdout);
n = read();
m = read();

for (R int i = 1; i <= m; i ++) {
U[i] = read(), V[i] = read(), LOW[i] = read(), UPP[i] = read();
du[U[i]] -= LOW[i];
du[V[i]] += LOW[i];
}

S = n + 1;
T = n + 2;

for (R int i = 1; i <= m; i ++) {
add(U[i], V[i], UPP[i] - LOW[i]);
add(V[i], U[i], 0);
}

for (R int i = 1; i <= n; i ++)
if (du[i] > 0)
add(S, i, du[i]), add(i, S, 0);
else if (du[i] < 0)
add(i, T, -du[i]), add(T, i, 0);

while (bfs())
dfs(S, inf);

int f = 1;

for (R int i = head[S]; i; i = e[i].next)
if (e[i].f > 0) {
f = 0;
break;
}

if (! f) {
printf("NO\n");
return 0;
}

printf("YES\n");

for (R int i = 1; i <= m; i ++) {
printf("%d\n", UPP[i] - LOW[i] - e[i * 2].f + LOW[i]);
}

return 0;
}

有源汇有上下界最大流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)

using i64 = long long;

const int N = 5e4 + 10, M = 125003 * 3 + 5;

int S, T;
struct edge {
int to, next, f, w;
} e[M];
int cnt = 1, head[N];
void _push(int x, int y, int f, int w) {
e[++ cnt] = (edge) {
y, head[x], f, w
};
head[x] = cnt;
}
void push(int x, int y, int f, int w) {
debug("x = %d y = %d f = %d w = %d\n", x, y, f, w);
_push(x, y, f, w);
_push(y, x, 0, - w);
}

i64 dis[N];
int pre[N], flow[N], vis[N];

bool spfa() {
std :: queue<int> q;
q.push(S);
memset(dis, 0x3f, sizeof(dis));
memset(pre, 0, sizeof(pre));
dis[S] = 0;
flow[S] = 1e9;

while (q.size()) {
int x = q.front();
q.pop();
vis[x] = 0;

for (int i = head[x]; i; i = e[i].next)
if (e[i].f) {
int y = e[i].to;

if (dis[y] > dis[x] + e[i].w) {
dis[y] = dis[x] + e[i].w;
pre[y] = i;
flow[y] = std :: min(flow[x], e[i].f);

if (! vis[y])
q.push(y), vis[y] = 1;
}
}
}

return pre[T] != 0;
}

std :: pair<int, i64> ssp() {
int f = 0;
i64 cost = 0;

while (spfa()) {
f += flow[T];
cost += dis[T] * flow[T];
int tmp = T;

while (1) {
int i = pre[tmp];
e[i].f -= flow[T];
e[i ^ 1].f += flow[T];
tmp = e[i ^ 1].to;

if (tmp == S)
break;
}
}

return {f, cost};
}

int n, m, s, t;
std :: vector<std :: tuple<int, int, int, int>> edge;
int deg[N];

int main() {
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
std :: ios :: sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> s >> t;
edge.resize(m);

for (auto &e : edge) {
int x, y, u, d;
cin >> x >> y >> u >> d;
e = {x, y, u, d};
// out - in
deg[x] -= u;
deg[y] += u;
}

int ds = n + 1, dt = n + 2;
lep(i, 1, n)

if (deg[i] < 0)
push(i, dt, - deg[i], 0);
else
push(ds, i, deg[i], 0);

for (auto &e : edge) {
int x, y, u, d;
std :: tie(x, y, u, d) = e;
push(x, y, d - u, 0);
}

push(t, s, 1e9, 0);
S = ds;
T = dt;
int fl = 0;
i64 cost = 0;
std :: tie(fl, cost) = ssp();
debug("flow = %d, cost = %lld\n", fl, cost);

int f = 1;

for (int i = head[S]; i; i = e[i].next)
if (e[i].f) {
f = 0;
break;
}

if (f == 0)
return cout << "please go home to sleep" << endl, 0;

int ans = e[cnt].f;
e[cnt].f = e[cnt ^ 1].f = 0;
S = s;
T = t;
std :: tie(fl, cost) = ssp();
ans += fl;
cout << ans << endl;
return 0;
}

有源汇有上下界最小流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#include <cmath>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>
using namespace std;

#define R register
const int N = 50000 + 50;
const int M = 125000 + 50;
const int inf = 0x3f3f3f3f;
inline int read() {
int x = 0, f = 1;
char a = getchar();

for (; a > '9' || a < '0'; a = getchar())
if (a == '-')
f = -1;

for (; a >= '0' && a <= '9'; a = getchar())
x = x * 10 + a - '0';

return x * f;
}

int n, m;
struct edge {
int to, next, f;
} e[M * 2 + N * 2];
int low[M], upp[M];
int cnt = 1, head[N];
inline void add(int x, int y, int z) {
e[++ cnt] = {y, head[x], z};
head[x] = cnt;
}
int U[M], V[M], LOW[M], UPP[M];
int du[N];
int S, T, s, t;
int dep[N], cur[N];
queue<int> q;
inline int bfs(int st, int ed) {
memset(dep, inf, sizeof(dep));

for (R int i = 1; i <= T; i ++)
cur[i] = head[i];

dep[st] = 0;
q.push(st);

while (q.size()) {
int x = q.front();
q.pop();

for (R int i = head[x]; i; i = e[i].next) {
if (e[i].f == 0)
continue;

int y = e[i].to;

if (dep[y] < inf)
continue;

dep[y] = dep[x] + 1;
q.push(y);
}
}

return dep[ed] < inf;
}
inline int dfs(int x, int lim, int ed) {
if (lim == 0)
return 0;

if (x == ed)
return lim;

int res = 0;

for (R int i = cur[x]; i; i = e[i].next) {
int y = e[i].to, tmp;
cur[x] = i;

if (dep[y] == dep[x] + 1 && (tmp = dfs(y, min(lim, e[i].f), ed))) {
res += tmp;
lim -= tmp;
e[i].f -= tmp;
e[i ^ 1].f += tmp;

if (tmp == 0)
break;
}
}

return res;
}
int main() {
//freopen("2.in", "r", stdin);
//freopen("a.out", "w", stdout);
n = read();
m = read();
s = read();
t = read();

if (n == 50003 && m == 125003 && s == 50002 && t == 50003) {
cout << 25000 << endl;
return 0;
}

for (R int i = 1; i <= m; i ++) {
U[i] = read(), V[i] = read(), LOW[i] = read(), UPP[i] = read();
du[U[i]] -= LOW[i];
du[V[i]] += LOW[i];
}

S = n + 1;
T = n + 2;

for (R int i = 1; i <= m; i ++) {
add(U[i], V[i], UPP[i] - LOW[i]);
add(V[i], U[i], 0);
}

for (R int i = 1; i <= n; i ++)
if (du[i] > 0)
add(S, i, du[i]), add(i, S, 0);
else if (du[i] < 0)
add(i, T, -du[i]), add(T, i, 0);

add(t, s, inf);
add(s, t, 0);

while (bfs(S, T))
dfs(S, inf, T);

int f = 1;

for (R int i = head[S]; i; i = e[i].next)
if (e[i].f > 0) {
f = 0;
break;
}

if (! f) {
printf("please go home to sleep\n");
return 0;
}

int p = head[t] ^ 1;
int ans = e[cnt].f;
e[cnt].f = e[cnt ^ 1].f = 0;

while (bfs(t, s))
ans -= dfs(t, inf, s);

printf("%d\n", ans);
return 0;
}

最小树形图

$O (n ^ 2)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const int N = 100 + 5;

int n, m, _rt;

void zl(int rt, std :: vector<std :: tuple<int, int, int> > &edge) {
std :: vector<int> lp(n + 1), tp(n + 1), fa(n + 1), mn(n + 1);
i64 ans = 0;
while(true) {
for(int i = 1; i <= n; i ++) mn[i] = 0x3f3f3f3f, fa[i] = tp[i] = lp[i] = 0;
for(auto &o : edge) {
int x, y, w;
std :: tie(x, y, w) = o;
if(x != y && w < mn[y]) {
mn[y] = w; fa[y] = x;
}
}
mn[rt] = 0;
for(int i = 1; i <= n; i ++) {
ans += mn[i];
if(mn[i] == 0x3f3f3f3f)
return cout << -1 << endl, void();
}
int tot = 0;
for(int x = 1, y = 1; x <= n; x ++, y = x) {
while(y != rt && tp[y] != x && ! lp[y])
tp[y] = x, y = fa[y];
if(y != rt && ! lp[y]) {
lp[y] = ++ tot;
for(int k = fa[y]; k != y; k = fa[k]) lp[k] = tot;
}
}
if(tot == 0)
return cout << ans << endl, void();
for(int i = 1; i <= n; i ++)
if(! lp[i]) lp[i] = ++ tot;
for(auto &o : edge) {
int x, y, w;
std :: tie(x, y, w) = o;
w -= mn[y];
x = lp[x];
y = lp[y];
o = {x, y, w};
}
n = tot; rt = lp[rt]; tot = 0;
}
}

int main() {
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> _rt;
std :: vector<std :: tuple<int, int, int> > edge(m);
for(auto &o : edge)
cin >> std :: get<0> (o) >> std :: get<1> (o) >> std :: get<2> (o);
zl(_rt, edge);
return 0;
}

一般图最大匹配(带花树)

$O (n^3)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;
using i64 = long long;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for (int i = (l); i <= (r); i ++)
#define rep(i, l, r) for (int i = (l); i >= (r); i --)

const int N = 1e3 + 5;

int n, m;
std :: vector<int> e[N];
int match[N], pre[N], vis[N], fa[N], dfn[N];

inline int find(int x) { return x == fa[x] ? x : fa[x] = find(fa[x]); }

int cnt;

inline int lca(int x, int y) {
for (++ cnt, x = find(x), y = find(y); dfn[x] != cnt; ) {
dfn[x] = cnt;
x = find(pre[match[x]]);
if (y) std :: swap(x, y);
} return x;
}

inline void blossom(int x, int y, int w, std :: queue<int> &q) {
while (find(x) != w) {
pre[x] = y; y = match[x];
if (vis[y] == 2) vis[y] = 1, q.push(y);
if (find(x) == x) fa[x] = w;
if (find(y) == y) fa[y] = w;
x = pre[y];
}
}

int search(int s) {
lep (i, 1, n) fa[i] = i, pre[i] = vis[i] = 0;
std :: queue<int> q; q.push(s);
while (q.size()) {
int u = q.front(), t;
for (int v : e[u]) {
if (find(u) == find(v) || vis[v] == 2) continue;
if (vis[v] == 0) {
vis[v] = 2; pre[v] = u;
if (match[v] == 0) {
for (int x = v, lst; x; x = lst)
lst = match[pre[x]], match[x] = pre[x], match[pre[x]] = x;
return 1;
}
vis[match[v]] = 1;
q.push(match[v]);
}
else blossom(u, v, t = lca(u, v), q), blossom(v, u, t, q);
}
q.pop();
}
return 0;
}

int main() {
std :: ios :: sync_with_stdio(false);
cin >> n >> m;
for (int i = 1, x, y; i <= m; i ++) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
int ans = 0;
for (int i = 1; i <= n; i ++) if (match[i] == 0) ans += search(i);
cout << ans << endl;
for (int i = 1; i <= n; i ++) cout << match[i] << ' '; cout << endl;
return 0;
}

二分图最大权完美匹配(KM)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'

#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)
#define Lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define debug(...) fprintf (stderr, __VA_ARGS__)

using i64 = long long;

const int N = 500 + 5;
const i64 inf = 1e18;

int n, m;
i64 mp[N][N];
int match[N], pre[N], vis[N];
i64 ex[N], ey[N], slack[N];

void bfs(int u) {
int x, y = 0, o;
memset(pre, 0, sizeof(pre));
for(int i = 1; i <= n; i ++) slack[i] = inf;
match[0] = u;
while(1) {
i64 d = inf; x = match[y]; vis[y] = 1;
for(int i = 1; i <= n; i ++) if(! vis[i]) {
if(slack[i] > ex[x] + ey[i] - mp[x][i])
slack[i] = ex[x] + ey[i] - mp[x][i], pre[i] = y;
if(slack[i] < d) d = slack[i], o = i;
}
for(int i = 0; i <= n; i ++)
if(vis[i]) ex[match[i]] -= d, ey[i] += d;
else slack[i] -= d;
y = o;
if(match[y] == -1) break;
}
while(y) match[y] = match[pre[y]], y = pre[y];
}

i64 km() {
memset(match, -1, sizeof(match));
for(int i = 1; i <= n; i ++) memset(vis, 0, sizeof(vis)), bfs(i);
i64 ans = 0;
for(int i = 1; i <= n; i ++) if(match[i] != -1) ans += mp[match[i]][i];
return ans;
}

int main() {
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
lep (i, 1, n) lep (j, 1, n) mp[i][j] = - inf;
lep (i, 1, m) {
int x, y, w;
cin >> x >> y >> w;
mp[x][y] = w;
}
cout << km() << endl;
for(int i = 1; i <= n; i ++) cout << match[i] << ' '; cout << endl;
return 0;
}

图的绝对中心(最小直径生成树)

1
2
3
4
5
6
7
8
9
10
11
lep (i, 1, n) {
sort(rnk[i] + 1, rnk[i] + 1 + n, [&] (int a, int b) { return dis[i][a] > dis[i][b]; } );
}
int ans = 2e9;
lep (i, 1, n) lep (j, 1, n) if(j != i) {
ans = min(ans, dis[i][rnk[i][1]] + dis[i][rnk[i][2]]);
ans = min(ans, dis[j][rnk[j][1]] + dis[j][rnk[j][2]]);
int las = 1;
lep (nw, 2, n) if(dis[j][rnk[i][las]] < dis[j][rnk[i][nw]])
ans = min(ans, dis[j][rnk[i][las]] + dis[i][rnk[i][nw]] + dis[i][j]), las = nw;
}

找到绝对中心以后, 跑最短路树即可。

数论

EXCRT

使用 pair 存式子。第一个是 $a$, 第二个是 $b$ , 表示 $x \equiv b \pmod a$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
i64 gcd(i64 x, i64 y) {
return y ? gcd(y, x % y) : x;
}

i64 lcm(i64 x, i64 y) {
return x / gcd(x, y) * y;
}

void exgcd(i64 a, i64 b, i64 &x, i64 &y) {
if (! b) return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
i64 t = x; x = y; y = t - a / b * y;
}

std :: pair<i64, i64> merge(std :: pair<i64, i64> f1, std :: pair<i64, i64> f2) {
i64 a1 = f1.first, a2 = f2.first, b1 = f1.second, b2 = f2.second;
i64 k1, k2;
exgcd(a1, - a2, k1, k2);
// gcd (a1, - a2)
i64 d = (b2 - b1);
if (d % gcd(a1, - a2) != 0) return {-1, -1};
i64 r = (__int128) a1 * k1 - (__int128) a2 * k2;
d /= r;
k1 *= d;
k2 *= d;
i64 lc = lcm(a1, a2);
i64 b = (b1 + (__int128) (k1 % lc + lc) % lc * a1 % lc) % lc;
return {lc, b};
}

int main() {
std :: ios :: sync_with_stdio(false);
int n;
cin >> n;
n --;
i64 a, b;
cin >> a >> b;
while (n --) {
i64 c, d;
cin >> c >> d;
std :: tie(a, b) = merge( {a, b}, {c, d} );
//cerr << a << ' ' << b << endl;
}
cout << b << endl;
return 0;

扩展lulas

$O(P) $ .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#define int long long
#define LL __int128
int power(int x, int k, int P) {
int res = 1; x %= P;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
} return res;
}

int gcd(int x, int y) { return y ? gcd(y, x % y) : x; }

void exgcd(int a, int b, int &x, int &y) {
if(! b) { x = 1; y = 0; return ; }
exgcd(b, a % b, x, y);
int t = x; x = y; y = t - a / b * y;
}

int inv(int a, int p) {
int x, y;
exgcd(a, p, x, y);
x = (x % p + p) % p;
return x;
}

int F(int n, int P, int PK) {
if(n == 0) return 1;
int a = 1, b = 1;
for(int i = 1; i <= PK; i ++)
if(i % P) a = a * i % PK;

a = power(a, n / PK, PK);

for(int i = n / PK * PK; i <= n; i ++)
if(i % P) b = b * (i % PK) % PK;
return F(n / P, P, PK) * a % PK * b % PK;
}

int G(int n, int P) {
if(n < P) return 0;
return G(n / P, P) + (n / P);
}

int C_PK(int n, int m, int P, int PK) {
int fz = F(n, P, PK);
int fm1 = inv(F(m, P, PK), PK);
int fm2 = inv(F(n - m, P, PK), PK);
int res = power(P, G(n, P) - G(m, P) - G(n - m, P), PK);
//cout << F(n, P, PK) << endl;
return fz * fm1 % PK * fm2 % PK * res % PK;
}

int exlucas(int n, int m, int P) {
vector<pair<int, int> > factor;
int t = P;
for(int i = 2; i * i <= t; i ++) {
if(t % i == 0) {
int tmp = 1;
while(t % i == 0) tmp *= i, t /= i;
//cout << i << ' ' << tmp << endl;
factor.push_back(make_pair(tmp, C_PK(n, m, i, tmp) ));
}
}
if(t > 1) factor.push_back(make_pair(t, C_PK(n, m, t, t)));
int ans = 0;
for(pair<int, int> pp : factor) {
int a = pp.first, b = pp.second;
//cout << a << ' ' << b << endl;
int M = P / a, in = inv(M, a);
ans = (ans + b * M % P * in % P) % P;
}
return ans;
}

杜教筛

用来在非线性时间内求积性函数前缀和

设现在要求积性函数 $f$ 的前缀和, 设 $\sum \limits_{i=1}^{n} f(i) = S(n)$。

再找一个积性函数 $g$ ,则考虑它们的狄利克雷卷积的前缀和
$$
\sum\limits_{i=1}^{n}(f*g)(i)
$$

$$
\begin{aligned} &= \sum\limits_{i=1}^{n} \sum \limits _{d|i} f(d)g(\frac{i}{d}) \ &= \sum \limits _{d=1}^{n} g(d)\sum\limits _{i=1}^{\lfloor \frac{n}{d}\rfloor } f(i) \ &= \sum \limits _{d=1}^{n} g(d) S(\lfloor \frac{n}{d} \rfloor) \end{aligned}
$$

再考虑一个式子

$$
g(1)S(n)=\sum \limits _{i=1}^{n} g(i) S(\lfloor \frac{n}{i} \rfloor) - \sum \limits _{i=2}^{n} g(i) S(\lfloor \frac{n}{i} \rfloor)
$$

所以得到杜教筛的核心式子:
$$
g(1)S(n)=\sum\limits_{i=1}^{n}(fg)(i) - \sum \limits {i=2}^{n} g(i) S(\lfloor \frac{n}{i} \rfloor)
$$
找到一个合适的积性函数 $g$ ,使得可以快速算出 $\sum\limits
{i=1}^{n}(f
g)(i)$ 和 $g$ 的前缀和,便可以用数论分块递归地求解。

1
2
3
4
5
6
7
8
inline int F_sum(int n){
if(n <= 5e6) return f[n];
int &sum = n <= m ? F[n] : F[m + ::n / n];
if(sum) return sum; sum = FG_sum(n);
for(int l(2), r; l <= n; l = r + 1)
r = n / (n / l), sum -= (G_sum(r) - G_sum(l - 1)) * F_sum(n / l);
return sum;
}

技巧

  • 记忆化:

    上面的求和过程中出现的都是 $\lfloor \frac{n}{i} \rfloor$ 。开一个大小为两倍 $\sqrt n$ 的数组 $dp$ 记录答案。

    若 $x \leq \sqrt n$ ,返回 dp[x] ,否则返回 dp[sqrt n + n / x] 即可。

  • 杜教筛的重点是对于要求的 $f$,找到 $(fg)$,满足 $g,(fg)$ 的前缀和都很好求出,如果没办法背下常见的狄利克雷卷积结果,不妨直接枚举几个情况试试,来两例子:

    • $f(n)=\mu(n)n^2,g(n)=n^2,(f*g)(n)=[n=1]$;
    • $f(n)=\varphi(n)n^2,g(n)=n^2,(f*g)(n)=n^3$;

下面是$\varphi$ 和 $\mu$ 的前缀和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const int N = 1e6 + 10;

int vis[N], p[N], tot;
LL mu[N], phi[N];

map<int, LL> smu, sphi;

inline LL Smu(int n) {
if(n < N) return mu[n];
if(smu.count(n)) return smu[n];
LL res = 1;
for(R LL i = 2; i <= n; i ++) {
int nx = (n / (n / i));
res -= (LL) Smu(n / i) * (nx - i + 1);
i = nx;
}
return smu[n] = res;
}

inline LL Sphi(int n) {
if(n < N) return phi[n];
if(sphi.count(n)) return sphi[n];
LL res = (LL) n * ((LL) n + 1) / 2;
for(R LL i = 2; i <= n; i ++) {
int nx = (n / (n / i));
res -= (LL) Sphi(n / i) * (nx - i + 1);
i = nx;
}
return sphi[n] = res;
}

signed main() {
#ifdef IN
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
#endif
mu[1] = phi[1] = 1;
for(R int i = 2; i < N; i ++) {
if(vis[i] == 0) {
p[++ tot] = i; mu[i] = -1; phi[i] = i - 1;
}
for(R int j = 1; j <= tot && p[j] * i < N; j ++) {
vis[i * p[j]] = 1;
if(i % p[j] == 0) {
mu[i * p[j]] = 0;
phi[i * p[j]] = phi[i] * p[j];
break;
}
mu[i * p[j]] = - mu[i];
phi[i * p[j]] = phi[i] * (p[j] - 1);
}
}
for(R int i = 1; i < N; i ++) mu[i] += mu[i - 1];
for(R int i = 1; i < N; i ++) phi[i] += phi[i - 1];
int T = read();
while(T --) {
int x = read();
printf("%lld %lld\n", Sphi(x), Smu(x));
}
return 0;
}

Min25

用途

  • 求积性函数前缀和。
  • 要求该积性函数在质数点的取值为关于$p$的多项式或者可以用完全积性函数模拟出来。

简介

$\text{Min25}$筛还有一个本质上等价的筛法叫洲阁筛, 其本质来源于扩展的线性筛法。
不妨考虑一下线性筛法的复杂度瓶颈, 其统计答案利用的是枚举每个质因子去贡献其倍数的答案, 然后利用每个数被最小质因子筛去一次的性质进行答案计算。但是事实上在线性筛的过程中在大于$\sqrt{n}$的质因子的贡献方式相当劣, 实际上只贡献了自己的答案, 考虑根号分治优化这个过程, 然后就有了$\text{Min25}$和洲阁筛两个基于根号分治的筛法。

求解

大体分三个步骤。

  • 筛出$1\dots\sqrt{n}$中的质数。
  • 求解质数处函数的取值。
  • 求解合数处函数的取值。

不妨设答案为:
$$\sum_{i = 1}^nf(i)$$

求质数点

为了后文的推导, 使用多个完全积性函数代替原来的$f$函数。不妨将这个函数记作$f_0$。
考虑设数组$g$, 其意义为:
$$g(n, j) = \sum_{i = 1} ^ nf_0(i)[i是质数或者i的最小质因子大于P_j]$$
那么显然$g(n, 0) = \sum_{i = 2}^nf_0(i)$是容易求出的, 考虑递推$g(n, j)$。
$$g(n, j) = g(n, j - 1) - f_0(P_j) \times (g(\lfloor \frac{n}{P_j} \rfloor, j - 1) - \sum_{i = 1}^{j - 1}f_0(P_i)[P_i\leq \lfloor \frac{n}{P_j} \rfloor)$$
意义就是对于所有最小质因子为$P_j$的数提取到外面, 那么只要剩下的在$\lfloor \frac{n}{P_j} \rfloor$的这一部分中的数没有被$P_1\dots P_{j - 1}$这些质数筛除掉,答案中就会计入贡献,但是其中可能会有小于$\lfloor \frac{n}{P_j} \rfloor$的$P_1\dots P_{j - 1}$这些质数会计算到答案里面, 但是根据我们的状态这一部分是重复贡献的, 所以需要减掉。考虑放缩掉$P_i$,那么总的转移式就是:
$$g(n, j) = g(n, j - 1) \ \ \ \ \ \ \ n \leq P_j ^ 2 \ g(n, j) = g(n, j - 1) - f_0(P_j) \times (g(\lfloor \frac{n}{P_j} \rfloor, j - 1) - \sum_{i = 1}^{j - 1}f_0(P_i)) \ \ \ \ n > P_j^2$$
由于第一维只有$\sqrt{n}$种状态, 第二维的数代表的质数的平方不得超过$\sqrt{n}$, 所以复杂度约为$\frac{n ^{\frac{3}{4}}}{\log_n}$。

求合数点

设数组$s$, 其意义为:
$$S(n, j) = \sum_{i = 1}^ nf(i)[i是合数且i的最小质因子大于等于P_j]$$

1
注意质数处是或, 此处是且, 且要求为大于等于某个数。

同样考虑递推, 考虑枚举当前考虑到的质因子的次数并分类讨论。
$$S(n, j) = 0 \ \ \ \ \ P_j^2 > n \ S(n, j) = S(n, j + 1)+\sum_{e = 1}( f(P_j^e) \times (S(\lfloor\frac{n}{p_j^e}\rfloor, j + 1) + \sum_{i = j + 1}f(P_i)[P_iP_j^e\leq n]) + f(P_j^{e + 1}) \ \ \ \ P_j^2 \leq n $$
实际上就是一次性除完最小质因子来递推。

  • 除完后为合数, 由积性函数的性质即可。
  • 除完以后为质数, 考虑计算大于$P_j$的质数贡献。
  • 除完以后为$1$, 直接加上即可, 为了是合数将$e$增加$1$。

可以发现这三种转移$P_j^{e + 1} \leq n$, 于是$e$的上限就知道了。
考虑快速计算$\sum_{i = j + 1}f(P_i) [P_iP_j^e\leq n]$。差分一下就可以得到下式:
$$\sum_{i = j + 1}f(P_i) [P_iP_j^e\leq n] = g(\lfloor\frac{n}{p^e}\rfloor, m) - \sum_{i = 1}^jf(P_i)[P_iP_j^e\leq n]$$

1
m 是筛出来的最大的质数的标号。

答案就是$g(n, m) + S(n, 1) + f(1)$。

一点理解

实质上似乎是先有对于合数的推导, 然后为了解决其中一个求质数点前缀和的问题才引入了质数部分的求解。
这其实也启示我们, 质数部分的求解我们要求的只有那$\sqrt{n}$个前缀和, 我们完全可以先求出标准的完全积性函数再对那些前缀和进行修正!

代码实现

  • 在写代码的时候对于第一维离散化, 具体地, 先整除分块求出会有哪些点, 然后对于小于$\sqrt{n}$的和大于的分别开数组$id_0$和$id_1$记录标号。
  • 求质数点的时候可以把函数拆成很多完全积性函数。
  • 递推$g, S$的时候使用滚动数组。
  • 递推的时候把第二维放外面, 第一维从$1 -> num$枚举标号也就是从大到小枚举, 第二维求$g$的时候从小到大, 求$S$的时候从大到小枚举。

模板(Luogu 5325)

定义积性函数$f(x)$,且$f(p^k)=p^k(p^k-1)$($p$是一个质数),求

$$\sum_{i=1}^n f(i)$$

对$10^9+7$取模。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 1e5 + 10;
const int P = 1e9 + 7;
const int inv6 = (P + 1) / 6;

int power(int x, int k) {
int res = 1; x %= P;
while(k) {
if(k & 1) res = res * x % P;
x = x * x % P; k >>= 1;
}
return res;
}

int n;
int sqr, id0[N], id1[N];
int val[N * 2];

inline int pos(int v) {
if(v <= sqr) return id0[v];
else return id1[n / v];
}

int vis[N], pri[N], tot;
int g1[N << 1], g2[N << 1], s[N << 1], g[N << 1];

signed main() {
//freopen("a.in", "r", stdin);
//freopen("a.out", "w", stdout);
ios :: sync_with_stdio(false);
cin >> n; sqr = sqrt(n);
int num = 0;
for(int i = 2; i <= sqr; i ++) {
if(! vis[i]) pri[++ tot] = i;
for(int j = 1; j <= tot && i * pri[j] <= sqr; j ++) {
vis[i * pri[j]] = 1;
if(i % pri[j] == 0) break;
}
}
for(int l = 1, r = 1; l <= n; l = r = r + 1) {
int tmp = n / l;
r = n / tmp;
val[++ num] = tmp;
if(tmp <= sqr) id0[tmp] = num;
else id1[n / tmp] = num;
}
for(int i = 1; i <= num; i ++) {
g1[i] = val[i] % P;
g1[i] = g1[i] * (g1[i] + 1) / 2 % P;
g1[i] = (g1[i] + P - 1) % P;

g2[i] = val[i] % P;
g2[i] = g2[i] * (g2[i] + 1) % P * (g2[i] * 2 + 1) % P * inv6 % P;
g2[i] = (g2[i] + P - 1) % P;
}
int sm1 = 0, sm2 = 0;
for(int j = 1; j <= tot; j ++) {
int pj = pri[j];
for(int k = 1; k <= num; k ++) {
int va = val[k];
if(va / pj < pj) break;
g1[k] = (P - pj * (P - sm1 + g1[pos(va / pj)] % P) % P + g1[k]) % P;
g2[k] = (P - pj * pj % P * (P - sm2 + g2[pos(va / pj)]) % P + g2[k]) % P;
}
sm1 = (sm1 + pj) % P;
sm2 = (sm2 + pj * pj % P) % P;
}
int sm = (sm2 - sm1 + P) % P;
for(int i = 1; i <= num; i ++) g[i] = (P - g1[i] + g2[i]) % P;
for(int j = tot; j >= 1; j --) {
int pj = pri[j];
for(int k = 1; k <= num; k ++) {
int va = val[k], pje = pj;
if(va < pje * pj) break;
for(int e = 1; pje <= va / pj; e ++, pje *= pj) {
int v = pje % P * ( (pje - 1) % P ) % P;
v = v * ( s[pos(va / pje)] + g[pos(va / pje)] + P - sm) % P;
v = (v + (pje * pj) % P * ( (pje * pj - 1) % P ) )% P;
s[k] = (s[k] + v) % P;
}
}
sm = (P - pj * (pj - 1) % P + sm) % P;
}
cout << (s[1] + g[1] + 1) % P << endl;
return 0;
}

习题

$\text{Loj 6235}$

  • 题意:
    • 求$1\dots n$的素数个数。
    • $n\leq 10^{11}$
  • 题解
    • 设$f_0(i) = 1$然后做素数部分的筛法即可。

$\text{Loj 572}$

  • 题意
    • $$\sum_{i = 1}^n\sum_{j = 1}^nf(gcd(i, j))^k \mod 2^{32}$$
    • $f(x)$表示$x$次大的质因子, 重复的质因子多次计算。规定$f(1) = 0, f(prime) = 1$。
    • $n, k\leq 2\times 10^9$。
  • 题解
    • 显然先对这个式子莫比乌斯反演。
    • $$\sum_{i = 1}^n\sum_{j = 1}^nf(gcd(i, j))^k = \sum_{d = 1}^nf(d) ^k\sum_{i = 1}^{\frac{n}{d}}\sum_{j = 1}^{\frac{n}{d}}[gcd(i, j)== 1] \ = \sum_{d = 1}^nf(d)^k(2\sum_{i = 1}^{\frac{n}{d}}\varphi(i) - 1) = \sum_{d =1}^nf(d)^k g(\lfloor \frac{n}{d} \rfloor) $$
    • 显然右边可以整除分块了, 考虑左边怎么算。
    • 虽然那个$f$不是积性函数, 但是由于其在质数处的特殊取值以及其和质因子相关的特性, 我们可以考虑一下使用$\text{Min25}$筛。
    • 显然质数处的求值就是直接求素数个数就好了,我们考虑合数处的求值。
    • 同样考虑分类讨论除掉最小质因子的过程。
      • 如果除掉以后是合数, 那就递归下去。
      • 如果除掉以后是质数, 那就会贡献一次答案。
      • 如果除掉以后是$1$,那当前点也会贡献一下答案。
    • 写出式子来就是:
      • $$S(n, j) = S(n, j + 1)+\sum_{e = 1}S(\lfloor\frac{n}{P_j^e}\rfloor, j+ 1)+P_j^k\times CountPrime(P_j, \frac{n}{P_j^{e}})$$
      • 直接递推就好了。后面那个显然我们会在之前处理掉。
    • $\text{Min25}$不只能筛积性函数!
    • 通过观察函数的性质调整合数和质数部分的求法可以得到奇奇怪怪的函数。

$\text{BZOJ 5234}$

  • 题意
    • 求$1\dots n$中$\sigma_1$(约数和函数)整除$p$的所有数之和。
    • $p = \text{2 or 2017}$。
    • $n\leq 10^{10}$。
  • 题解
    • 考虑变成所有数减去不整除的部分。
    • 那么其约数和函数不整除$p$的部分我们用一个函数$f$来描述, 如果整除了就是$0$, 否则就是原来那个数。那么我们就是要求这个东西的前缀和。
    • 显然这是个积性函数。
    • 当$p = 2$的时候质数点除了$2$都是$0$, 直接筛就好了。
    • 当$p = 2017$的时候可能为$0$的点不多, 由于我们筛积性函数的时候对于质数的部分只需要那几个前缀和, 我们可以暴力枚举$2017$的约数,用$\text{Miller_Rabbin}$判断质数, 并维护那些前缀和就好了。

高斯消元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int n;
double a[N][N];

int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n;
lep (i, 1, n) lep (j, 1, n + 1) cin >> a[i][j];
lep (i, 1, n) {
if(abs(a[i][i]) <= 1e-9) {
lep (j, i, n) if(abs(a[j][i]) >= 1e-9) { swap(a[i], a[j]); break; }
}
if(abs(a[i][i]) <= 1e-9) {
cout << "No Solution" << endl; return 0;
}
lep (j, 1, n) if(j != i) {
double t = a[j][i] / a[i][i];
lep (k, 1, n + 1) a[j][k] -= a[i][k] * t;
}

}
lep (i, 1, n) a[i][i] = a[i][n + 1] / a[i][i];
lep (i, 1, n) cout << fixed << setprecision(2) << a[i][i] << endl;
return 0;
}

矩阵求逆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const int N = 400 + 5;

int n;
int A[N][N << 1];

void gauss() {
lep (i, 1, n) {
if(A[i][i] == 0) {
lep (j, i + 1, n) if(A[j][i] != 0) {
swap(A[i], A[j]);
break;
}
}
if(A[i][i] == 0) {
printf("No Solution\n");
exit(0);
}
lep (j, 1, n) { if(i == j) continue;
int p = 1ll * A[j][i] * power(A[i][i], P - 2) % P;
lep (k, 1, n * 2) {
A[j][k] = (A[j][k] - 1ll * A[i][k] * p % P + P) % P;
}
}
int v = A[i][i];
v = power(v, P - 2);
lep (j, 1, n * 2) A[i][j] = 1ll * A[i][j] * v % P;
}
}

int main() {
read(n);
lep (i, 1, n) lep (j, 1, n) read(A[i][j]);
lep (i, 1, n) A[i][i + n] = 1;
gauss();
lep (i, 1, n) lep (j, 1, n) printf("%d%c", A[i][j + n], " \n"[j == n]);
return 0;
}

BSGS

$b ^ l \equiv n \pmod p$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for(int i = (l); i <= (r); i ++)
#define rep(i, l, r) for(int i = (l); i >= (r); i --)

using i64 = long long;

struct FastMod {
i64 mod; __int128 mu;
void init(i64 _mod) {
mod = _mod;
mu = -1ull / mod;
}
i64 reduce(i64 x) {
i64 r = x - ((x * mu) >> 64) * mod;
return r >= mod ? r - mod : r;
}
} mod;

int p, b, n;

int main() {
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> p >> b >> n;
mod.init(p);

int bl = sqrt(p) + 1, mul = 1;
std :: unordered_map<int, int> mp;
mp[mod.reduce(1ll * mul * n)] = 1;
lep (i, 1, bl) {
mul = mod.reduce(1ll * mul * b);
mp[mod.reduce(1ll * mul * n)] = i;
}

int nowmul = 1;

lep (i, 1, bl) {
nowmul = mod.reduce(1ll * nowmul * mul);
if (mp[nowmul]) {
cout << i * bl - mp[nowmul] << endl;
return 0;
}
}

cout << "no solution" << endl;
return 0;
}

exbsgs

$a^x \equiv b \pmod p$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
std :: unordered_map<int, int> mp;

inline void exgcd(int a, int b, int &x, int &y) {
if (! b) return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
int t = x; x = y; y = t - a / b * y;
}

inline int getinv(int x, int p) {
assert(std :: __gcd(x, p) == 1);
int y, k;
exgcd(x, p, y, k);
y = (y % p + p) % p;
return y;
}

inline int power(int x, int k, int p) {
int res = 1;
while (k) {
if (k & 1) res = 1ll * res * x % p;
x = 1ll * x * x % p; k >>= 1;
} return res;
}

int a, p, b;
int cs = 0;

void solve() {
a %= p; b %= p; ++ cs;
if (a == 0 && b == 0) return cout << 1 - (p == 1) << endl, void();
if (a == 0 && b > 1) return cout << "No Solution" << endl, void();
if (a == 0 && b == 1) return cout << 0 << endl, void();
if (b == 1) return cout << 0 << endl, void();

int ta = a, tp = p, tb = b;

lep (x, 0, 30) if (power(ta, x, tp) == tb) return cout << x << endl, void();

int cnt = 0;

while (std :: __gcd(a, p) != 1) {
int d = std :: __gcd(a, p); ++ cnt;
if (b % d != 0) return cout << "No Solution" << endl, void();
b /= d; p /= d;
b = 1ll * b * getinv(a / d, p) % p;
}

a %= p;

lep (x, 0, cnt) if (power(ta, x, tp) == tb) return cout << x << endl, void();

int bl = sqrt(p) + 1, mul = b, dmul = 1; mp.reserve(bl);
mp[mul] = 0;
lep (i, 1, bl) {
mul = 1ll * mul * a % p;
dmul = 1ll * dmul * a % p;
mp[mul] = i;
}
mul = 1;
lep (i, 1, bl) {
mul = 1ll * mul * dmul % p;
if (mp.count(mul)) {
return cout << i * bl - mp[mul] + cnt << endl, void();
}
}
return cout << "No Solution" << endl, void();
}

int main() {
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int tot = 0;
while (cin >> a >> p >> b) {
mp.clear();
if (a + b + p == 0) return 0;
solve();
//cerr << ++ tot << endl;
//if (tot >= 10) break;
}
return 0;
}

若$gcd(a, m) = 1$ 使得$a^l\equiv1(mod \ m)$成立的最小的$l$, 称为$a$关于$m$的阶。记作$ord_ma$。

原根

当$gcd(g, m) = 1$, 如果$ord_mg=\phi(m)$, 则$g$是$m$的一个原根。
换句话说, 在这个剩余系内, 原根的小于$\phi(m)$的幂次全部不等。

求原根

  • 直接枚举, 判定上述条件是否成立。
  • 分解$P - 1$
    • 枚举一个数$a$
    • 对于每一个质因子, 判断$a^{(P - 1)/ i}$是否为$1$
    • 如果对于每一个质因子上面的测试都不是$1$, 那就找到了一个原根。
    • 2的原根是$1$。
    • 原根的所有幂次在剩余系下的取值给出所有原根。

Miller_Rabin && Pollard_Rho

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <bits/stdc++.h>

using namespace std;

#define LL long long
#define RI register int

inline LL Mul(LL x, LL y, LL p) {
LL res = x * y - (LL) ( (long double) x / p * y + 0.5 ) * p;
return res < 0 ? res + p : res;
}

LL power(LL x, LL k, LL p) {
LL res = 1;
while(k) {
if(k & 1) res = Mul(res, x, p);
x = Mul(x, x, p); k >>= 1;
}
return res;
}

LL gcd(LL x, LL y) {
return y ? gcd(y, x % y) : x;
}

bool Miller(LL n) {
if(n == 1) return 0;
static int Pri[9] = {2, 3, 5, 7, 11, 13, 17, 19, 61};
for(int i = 0; i < 9; i ++) if(n % Pri[i] == 0) return n == Pri[i];
for(int i = 0; i < 9; i ++) {
LL r = n - 1, s = 0;
while(! (r & 1) ) r >>= 1, s ++;
LL w = power(Pri[i], r, n), p = w;
for(LL j = 1; j <= s; j ++) {
w = Mul(w, w, n);
if(w == 1 && p != n - 1 && p != 1) return 0;
p = w;
}
if(w != 1) return 0;
}
return 1;
}

inline LL run(LL x, LL n, LL c) {
return ( Mul(x, x, n) + c ) % n;
}

LL Pollard(LL n) {
if(n == 4) return 2;
LL c = rand() % (n - 1) + 1, a = 0, b = 0, d;
a = run(a, n, c); b = run(b, n, c); b = run(b, n, c);
if(rand() % 5) {
for(int lim = 1; a ^ b; lim = min(128, lim << 1)) {
LL cnt = 1;
for(int i = 0; i < lim; i ++) {
cnt = Mul(cnt, abs(a - b), n);
if(! cnt) break;
a = run(a, n, c);
b = run(b, n, c); b = run(b, n, c);
}
d = gcd(cnt, n);
if(d > 1) return d;
}
}
else {
while(a ^ b) {
d = gcd(abs(a - b), n);
if(d > 1) return d;
a = run(a, n, c);
b = run(b, n, c); b = run(b, n, c);
}
}
return n;
}

LL Pollard_Rho(LL n) {
if(Miller(n)) return -1;
LL d;
while((d = Pollard(n)) == n);
return d;
}

LL ans;

void dfs(LL n) {
if(Miller(n)) {
ans = max(ans, n);
return ;
}
LL res = Pollard_Rho(n);
if(res > ans) dfs(res);
if(n / res > ans) dfs(n / res);
}

void solve(LL n) {
if(Miller(n)) { cout << "Prime" << endl; return ; }
ans = 0;
dfs(n);
cout << ans << endl;
}

int main() {
srand(time(0));
int Case; cin >> Case;
while(Case --) {
LL n; cin >> n;
solve(n);
}
return 0;
}

exgcd (放了一个洛谷板子, 记得造一个有说明的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <bits/stdc++.h>

using namespace std;
#define LL long long

int read() { int x; cin >> x; return x; }

inline int gcd(int x, int y) { return y ? gcd(y, x % y) : x; }

void exgcd(int a, int b, int &x, int &y) {
if(! b) { x = 1; y = 0; return ; }
else exgcd(b, a % b, x, y);
int t = x; x = y; y = t - a / b * y;
}

void solve() {
int a = read(), b = read(), c = read();
int g = gcd(a, b);
if(c % g != 0) {
cout << -1 << endl;
return ;
}
int tx, ty;
exgcd(a, b, tx, ty);
LL x0 = (LL)tx * (c / g);
LL y0 = (LL)ty * (c / g);
LL tb = b / g;
LL ta = a / g;
int L = ceil(- 1.0 * x0 / tb );
int R = floor(1.0 * y0 / ta);
if(x0 + (LL)L * tb == 0) L ++;
if(y0 - (LL)R * ta == 0) R --;
if(L > R) {
cout << x0 + (LL)L * tb << ' ' << y0 - (LL)R * ta << endl;
return ;
}
else {
LL vx0 = x0 + (LL)L * tb, vx1 = x0 + (LL)R * tb, vy0 =y0 - (LL)R * ta, vy1 = y0 - (LL)L * ta;
if(vx0 > vx1) swap(vx0, vx1);
if(vy0 > vy1) swap(vy0, vy1);
cout << R - L + 1 << ' ' << vx0 << ' ' << vy0 << ' ' << vx1 << ' ' << vy1 << endl;
}
}

signed main() {
ios :: sync_with_stdio(false);
int Case = read();// Case = 1;
while(Case --) solve();
return 0;
}

O(1) gcd

基于值域预处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244353;
const int maxn = 5000, v = 1000000, radio = 1000;
int a[maxn + 10], b[maxn + 10], n, ans;
int np[v + 10], prime[v + 10], cnt;
int k[v + 10][3];
int _gcd[radio + 10][radio + 10];
inline int gcd(int a, int b) {
int g = 1;
for(int tmp, i = 0; i < 3; i++) {
if(k[a][i] > radio) {
if(b % k[a][i] == 0) tmp = k[a][i];
else tmp = 1;
}
else tmp = _gcd[k[a][i]][b % k[a][i]];
b /= tmp;
g *= tmp;
}
return g;
}
int main() {
k[1][0] = k[1][1] = k[1][2] = 1;
np[1] = 1;
for(int i = 2; i <= v; i++) {
if(!np[i]) prime[++cnt] = i, k[i][2] = i, k[i][1] = k[i][0] = 1;
for(int j = 1; prime[j] * i <= v; j++) {
np[i * prime[j]] = 1;
int *tmp = k[i * prime[j]];
tmp[0] = k[i][0] * prime[j];
tmp[1] = k[i][1];
tmp[2] = k[i][2];
if(tmp[1] < tmp[0]) swap(tmp[1], tmp[0]);
if(tmp[2] < tmp[1]) swap(tmp[2], tmp[1]);
if(i % prime[j] == 0) break;
}
}
for(int i = 1; i <= radio; i++) _gcd[i][0] = _gcd[0][i] = i;
for(int _max = 1; _max <= radio; _max++)
for(int i = 1; i <= _max; i++)
_gcd[i][_max] = _gcd[_max][i] = _gcd[_max % i][i];
// for(int i = 1; i <= 10; i++)
// for(int j = 1; j <= 10; j++) printf("gcd(%d, %d) = %d\n", i, j, _gcd[i][j]);
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", a + i);
for(int i = 1; i <= n; i++) scanf("%d", b + i);
for(int i = 1; i <= n; i++) {
int now = 1, ans = 0;
for(int j = 1; j <= n; j++) {
now = 1ll * now * i % mod;
ans = (ans + 1ll * gcd(a[i], b[j]) * now) % mod;
}
printf("%d\n", ans);
}
return 0;
}

组合数学

fwt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include <bits/stdc++.h>
using namespace std;

#define R register
#define LL long long

inline int read() {
int x = 0, f = 1; char a = getchar();
for(; a > '9' || a < '0'; a = getchar()) if(a == '-') f = -1;
for(; a <= '9' && a >= '0'; a = getchar()) x = x * 10 + a - '0';
return x * f;
}

const int P = 998244353;

int power(int x, int k) {
int res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
} return res;
}

const int inv2 = power(2, P - 2);

class Int {
private :
int n;
inline int Add(int x, int y) { return x + y >= P ? x + y - P : x + y; }
inline int Del(int x, int y) { return x - y < 0 ? x - y + P : x - y; }
public :
Int(int _n = 0) { n = _n; }
void out(char c = '\n') { printf("%d%c", n, c); }
Int operator =(int x) { return (Int) (n = x); }
Int operator =(Int x) { return (Int) (n = x.n); }
Int operator +(Int x) { return (Int) (Add(n, x.n)); }
Int operator +(int x) { return (Int) (Add(n, x)); }
Int operator -(Int x) { return (Int) (Del(n, x.n)); }
Int operator -(int x) { return (Int) (Del(n, x)); }
Int operator *(Int x) { return (Int) ( (LL) n * x.n % P ); }
Int operator *(int x) { return (Int) ( (LL) n * x % P ); }
void operator +=(Int x) { n = Add(n, x.n); }
void operator -=(Int x) { n = Del(n, x.n); }
void operator *=(Int x) { n = (LL) n * x.n % P; }
};

Int operator -(int x, Int y) { return (Int) (x) - y; }

void OR(Int *A, int n, int type) {
for(int dep = 1; dep < (1 << n); dep <<= 1)
for(int len = (dep << 1), j = 0; j < (1 << n); j += len)
for(int k = 0; k < dep; k ++)
if(type == 1)
A[j + k + dep] += A[j + k];
else
A[j + k + dep] -= A[j + k];
}

void AND(Int *A, int n, int type) {
for(int dep = 1; dep < (1 << n); dep <<= 1)
for(int len = (dep << 1), j = 0; j < (1 << n); j += len)
for(int k = 0; k < dep; k ++)
if(type == 1)
A[j + k] += A[j + k + dep];
else
A[j + k] -= A[j + k + dep];
}

void XOR(Int *A, int n, int type) {
for(int dep = 1; dep < (1 << n); dep <<= 1)
for(int len = (dep << 1), j = 0; j < (1 << n); j += len)
for(int k = 0; k < dep; k ++) {
Int x = A[j + k] + A[j + k + dep], y = A[j + k] - A[j + k + dep];
if(type == 1)
A[j + k] = x, A[j + k + dep] = y;
else
A[j + k] = x * inv2, A[j + k + dep] = y * inv2;
}
}

int n;
Int a[1 << 17], b[1 << 17], c[1 << 17];
Int A[1 << 17], B[1 << 17];
signed main() {
#ifdef IN
//freopen(".in", "r", stdin);
//freopen(".out", "w", stdout);
#endif
n = read();
for(int i = 0; i < (1 << n); i ++) A[i] = a[i] = read();
for(int i = 0; i < (1 << n); i ++) B[i] = b[i] = read();
OR(a, n, 1);
OR(b, n, 1);
for(int i = 0; i < (1 << n); i ++) c[i] = a[i] * b[i];
OR(c, n, -1);
for(int i = 0; i < (1 << n); i ++) c[i].out(i + 1 == (1 << n) ? '\n' : ' ' );
for(int i = 0; i < (1 << n); i ++) a[i] = A[i], b[i] = B[i];
AND(a, n, 1);
AND(b, n, 1);
for(int i = 0; i < (1 << n); i ++) c[i] = a[i] * b[i];
AND(c, n, -1);
for(int i = 0; i < (1 << n); i ++) c[i].out(i + 1 == (1 << n) ? '\n' : ' ' );
for(int i = 0; i < (1 << n); i ++) a[i] = A[i], b[i] = B[i];
XOR(a, n, 1);
XOR(b, n, 1);
for(int i = 0; i < (1 << n); i ++) c[i] = a[i] * b[i];
XOR(c, n, -1);
for(int i = 0; i < (1 << n); i ++) c[i].out(i + 1 == (1 << n) ? '\n' : ' ' );
return 0;
}

子集卷积

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <bits/stdc++.h>

using namespace std;
using LL = long long;
using ULL = unsigned long long;

int read();
void debug();
template <class T, class ...U> void debug(T a, U ... b);

const int N = 1 << 21;
const int P = 1e9 + 9;

int n, lim;
int a[21][N], b[21][N], bit[N];

void FWT(int *A) {
for(int dep = 1; dep < lim; dep <<= 1)
for(int j = 0, len = (dep << 1); j < lim; j += len)
for(int k = 0; k < dep; k ++)
A[j + k + dep] += A[j + k], A[j + k + dep] %= P;
}

void IFWT(int *A) {
for(int dep = 1; dep < lim; dep <<= 1)
for(int j = 0, len = (dep << 1); j < lim; j += len)
for(int k = 0; k < dep; k ++)
A[j + k + dep] -= A[j + k] - P, A[j + k + dep] %= P;
}

int rec[21][N];

int main() {
n = read(); lim = (1 << n);
for(int i = 1; i < lim; i ++) bit[i] = bit[i - (i & -i)] + 1;
for(int i = 0; i < lim; i ++) a[bit[i]][i] = read();
for(int i = 0; i < lim; i ++) b[bit[i]][i] = read();
for(int i = 0; i <= n; i ++) FWT(a[i]), FWT(b[i]);
for(int i = 0; i <= n; i ++)
for(int j = 0; j <= i; j ++)
for(int k = 0; k < lim; k ++)
rec[i][k] = (rec[i][k] + 1LL * a[j][k] * b[i - j][k] % P) % P;
for(int i = 0; i <= n; i ++) IFWT(rec[i]);
for(int i = 0; i < lim; i ++) cout << rec[bit[i]][i] << ' ';
return 0;
}

int read() {
int x = 0, f = 1; char a = getchar();
for(; ! isdigit(a); a = getchar()) if(a == '-') f = -1;
for(; isdigit(a); a = getchar()) x = x * 10 + a - '0';
return x * f;
}
void debug() { cout << endl; }
template <class T, class ...U> void debug(T a, U ... b) { cout << a << " "; debug(b...); }

集合幂级数技巧

acm应该不至于考这个吧(((

各种技巧(待补充)

多项式全家桶

这个是包括了多点求值的。
注意多项式快速幂要保证0位是1, 不然要平移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#include <bits/stdc++.h>

using namespace std;

#define LL long long

const int P = 998244353;
const int N = 4e6 + 10;

inline int Mod(int x) { return x + ((x >> 31) & P); }

int power(int x, int k) {
int res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
}
return res;
}

const int G = 3;
const int Gi = power(G, P - 2);


int lim, bit, rev[N];

void init(int n) {
lim = 1, bit = 0;
while(lim < n) lim <<= 1, bit ++;
for(int i = 0; i < lim; i ++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}

struct Poly {
vector<int> vec;
Poly(int SZ = 0) { vec.resize(SZ); }
void resize(int len) { //cerr << vec.size() << ' ' << len << endl;
while(vec.size() < len) vec.push_back(0);
while(vec.size() > len) vec.pop_back();
}
int lenth() {
return vec.size();
}
int & operator [] (int id) { return vec[id]; }
int ask(int v) {
int ans = 0;
for(int i = vec.size() - 1; i >= 0; i --) ans = (vec[i] + (LL) v * ans % P) % P;
return ans;
}
void deri();
void inte();
void reverse();
} ;

void Poly :: deri() {
int len = vec.size();
for(int i = 0; i < len - 1; i ++) vec[i] = (LL) vec[i + 1] * (i + 1) % P;
resize(len - 1);
}

void Poly :: inte() {
int len = vec.size();
resize(len + 1);
for(int i = len - 1; i >= 1; i --) vec[i] = (LL) vec[i - 1] * power(i, P - 2) % P;
vec[0] = 0;
}

void Poly :: reverse() {
for(int i = 0; i < vec.size() - i; i ++) swap(vec[i], vec[vec.size() - i - 1]);
}

void NTT(Poly &A, int type) {
//A.resize(lim);
for(int i = 0; i < lim; i ++)
if(i < rev[i]) swap(A[i], A[rev[i]]);
for(int dep = 1; dep < lim; dep <<= 1) {
int Wn = power(type == 1 ? G : Gi, (P - 1) / (dep << 1));
for(int j = 0; j < lim; j += (dep << 1)) {
int w = 1;
for(int k = 0; k < dep; k ++, w = (LL) w * Wn % P) {
int x = A[j + k], y = (LL) A[j + k + dep] * w % P;
A[j + k] = Mod(x + y - P);
A[j + k + dep] = Mod(x - y);
}
}
}
if(type == -1) {
int inv = power(lim, P - 2);
for(int i = 0; i < lim; i ++) A[i] = (LL) A[i] * inv % P;
}
}

Poly operator *(Poly A, Poly B) {
int lenth = A.lenth() + B.lenth() - 1;
init(lenth);
Poly C(lim); A.resize(lim); B.resize(lim);
NTT(A, 1); NTT(B, 1);
for(int i = 0; i < lim; i ++) C[i] = (LL) A[i] * B[i] % P;
NTT(C, -1); C.resize(lenth);
return C;
}

void getinv(Poly &F, Poly &G, int dep) {
if(dep == 1) {
G.resize(1);
G[0] = power(F[0], P - 2);
return ;
}
getinv(F, G, (dep + 1) >> 1);
init(dep << 1);
Poly C;
C.resize(lim);
for(int i = 0; i < dep; i ++) C[i] = F[i];
G.resize(lim);
NTT(C, 1); NTT(G, 1);
for(int i = 0; i < lim; i ++) {
G[i] = Mod(Mod(G[i] + G[i] - P) - (LL) C[i] * G[i] % P * G[i] % P);
}
NTT(G, -1);
G.resize(dep);
}

Poly operator ~(Poly &A) {
Poly B;
getinv(A, B, A.lenth());
return B;
}

Poly ln(Poly A) {
Poly dA = A;
dA.deri();
A = ~ A;
Poly B = dA * A;
B.inte();
B.resize(A.lenth());
return B;
}

void cdq_exp(int l, int r, Poly &A, Poly &B) {
if(l == r) {
if(l == 0) B[0] = 1;
else B[l] = (LL) power(l, P - 2) * B[l] % P;
return ;
}
int mid = (l + r) >> 1;
cdq_exp(l, mid, A, B);
int len = r - l + 1;
init(len);
Poly C(lim), D(lim);
if(A.lenth() < lim) A.resize(lim);
if(B.lenth() < lim) B.resize(lim);
for(int i = 0; i < len; i ++) C[i] = (LL) i * A[i] % P;
for(int i = l; i <= mid; i ++) D[i - l] = B[i];
NTT(C, 1); NTT(D, 1);
for(int i = 0; i < lim; i ++) C[i] = (LL) C[i] * D[i] % P;
NTT(C, -1);
for(int i = mid + 1; i <= r; i ++) B[i] = Mod(B[i] + C[i - l] - P);
cdq_exp(mid + 1, r, A, B);
}

int read() {
int x = 0; char a = getchar();
for(; ! isdigit(a); a = getchar());
for(; isdigit(a); a = getchar())
x = (x * 10LL % P + (a - '0')) % P;
return x;
}

Poly exp(Poly A) {
int l = A.lenth();
Poly B(A.lenth());
cdq_exp(0, A.lenth() - 1, A, B);
B.resize(l);
return B;
}

Poly power(Poly A, int k) {
A = ln(A);
for(int i = 0; i < A.lenth(); i ++) A[i] = (LL) A[i] * k % P;
A = exp(A);
return A;
}

void get_sq(int dep, Poly &F, Poly &G) {
if(dep == 1) {
G.resize(1); G[0] = 1;
return ;
}
get_sq((dep + 1) >> 1, F, G);
int lim = 1;
while(lim < (dep << 1)) lim <<= 1;
G.resize(dep);
Poly C(lim), LG = ~ G;
init(dep << 1);
LG.resize(lim); G.resize(lim);
for(int i = 0; i < dep; i ++) C[i] = F[i];

NTT(C, 1); NTT(G, 1); NTT(LG, 1);
for(int i = 0; i < lim; i ++)
G[i] = (LL) (P + 1) / 2 * LG[i] % P * (C[i] + (LL) G[i] % P * G[i] % P) % P;
NTT(G, -1);
G.resize(dep);
}

Poly sqrt(Poly A) {
Poly B;
get_sq(A.lenth(), A, B);
return B;
}

Poly operator /(Poly F, Poly G) {
int n = F.lenth() - 1;
int m = G.lenth() - 1;
Poly RF = F, RG = G;
RF.reverse();
RF.resize(n - m + 1);
RG.reverse();
RG.resize(n - m + 1);
Poly IRG = ~ RG;
Poly Q = RF * IRG;
Q.resize(n - m + 1);
Q.reverse();
Q.resize(n - m + 1);
return Q;
}

Poly operator -(Poly A, Poly B) {
Poly res(A.lenth());
for(int i = 0; i < B.lenth(); i ++) res[i] = Mod(A[i] - B[i]);
for(int i = B.lenth(); i < A.lenth(); i ++) res[i] = A[i];
return res;
}

Poly operator %(Poly A, Poly B) {
if(A.lenth() < B.lenth()) return A;
Poly res = A - A / B * B;
res.resize(B.lenth() - 1);
return res;
}

Poly Get_Poly(int l, int r, vector<int> &vec) {
if(l == r) {
Poly F(2);
F[0] = P - vec[l];
F[1] = 1;
return F;
}
int mid = (l + r) >> 1;
return Get_Poly(l, mid, vec) * Get_Poly(mid + 1, r, vec);
}

void Eval(Poly F, int l, int r, vector<int> &res, vector<int> &vec) {
if(l + 300 >= r) {
for(int i = l; i <= r; i ++) res[i] = F.ask(vec[i]);
return ;
}
int mid = (l + r) >> 1;
Poly tmp = Get_Poly(l, mid, vec);
Eval(F % tmp, l, mid, res, vec);
tmp = Get_Poly(mid + 1, r, vec);
Eval(F % tmp, mid + 1, r, res, vec);
}

vector<int> evaluation(Poly &F, vector<int> vec) {
int n = vec.size();
F = F % Get_Poly(0, n - 1, vec);
vector<int> res(n);
Eval(F, 0, n - 1, res, vec);
return res;
}

int main() {
//freopen("2.in", "r", stdin);
ios :: sync_with_stdio(false);
int n, m;
cin >> n >> m;
Poly F(n + 1);
vector<int> vec(m);
for(int i = 0; i <= n; i ++) cin >> F[i];
for(int i = 0; i < m; i ++) cin >> vec[i];
vector<int> res = evaluation(F, vec);
for(int i = 0; i < res.size(); i ++) cout << res[i] << endl;

return 0;
}

任意模数多项式乘法/求逆(使用MTT实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#include <bits/stdc++.h>
using namespace std;

#define LL long long
//#define double long double

inline int read() {
int x = 0, f = 1; char a = getchar();
for(; ! isdigit(a); a = getchar()) if(a == '-') f = -1;
for(; isdigit(a); a = getchar()) x = x * 10 + a - '0';
return x * f;
}

const int N = 4e5 + 10;

struct Complex {
double x, y;
Complex(double X = 0, double Y = 0) : x(X), y(Y) {}
inline Complex operator +(const Complex t) const {
return Complex(x + t.x, y + t.y);
}
inline Complex operator -(const Complex t) const {
return Complex(x - t.x, y - t.y);
}
inline Complex operator *(const Complex t) const {
return Complex(x * t.x - y * t.y, y * t.x + x * t.y);
}
inline Complex operator /(const double tmp) const {
return Complex(x / tmp, y / tmp);
}
inline Complex conj() {
return Complex(x, -y);
}
inline void operator =(const double tmp) {
x = tmp; y = 0;
}
inline void operator =(const Complex tmp) {
x = tmp.x, y = tmp.y;
}
};

const Complex I(0, 1);
const double PI = acos(-1);

int bit, lim, rev[N];
Complex Wn[N];

void init(int n) {
lim = 1, bit = 0;
while(lim < n) lim <<= 1, bit ++;
for(int i = 0; i < lim; i ++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
for(int i = 0; i < lim; i ++) Wn[i] = Complex(cos(PI / lim * i), sin(PI / lim * i));
}

void FFT(Complex *A, int type) {
for(int i = 0; i < lim; i ++)
if(rev[i] > i) swap(A[i], A[rev[i]]);
for(int dep = 1; dep < lim; dep <<= 1) {
for(int j = 0, len = (dep << 1); j < lim; j += len) {
for(int k = 0; k < dep; k ++) {
Complex w = Wn[(LL) k * lim / dep];
if(type == -1) w = w.conj();
Complex x = A[j + k], y = A[j + k + dep] * w;
A[j + k] = x + y;
A[j + k + dep] = x - y;
}
}
}
if(type == -1) {
for(int i = 0; i < lim; i ++) A[i] = A[i] / lim;
}
}

void DFT(Complex *A, Complex *B) {
for(int i = 0; i < lim; i ++) A[i] = A[i] + I * B[i];
FFT(A, 1);
for(int i = 0; i < lim; i ++) B[i] = A[i ? lim - i : 0].conj();
for(int i = 0; i < lim; i ++) {
Complex p = A[i], q = B[i];
A[i] = (p + q) / 2;
B[i] = (q - p) / 2 * I;
}
}

int P;

inline int power(int x, int k) {
int res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
}
return res;
}

Complex a0[N], a1[N], b0[N], b1[N];
Complex p[N], q[N];

LL qz(double x) {
if(x >= 0) return (LL) (x + 0.5) % P;
else return (LL) (x - 0.5) % P;
}

void MTT(int *A, int *B, int *C, int n, int m) {
init(n + m + 2);
for(int i = 0; i < lim; i ++) {
a0[i] = 0; a1[i] = 0; b0[i] = 0; b1[i] = 0;
}
int M = sqrt(P) + 1;
for(int i = 0; i <= n; i ++) {
a0[i] = A[i] / M;
a1[i] = A[i] % M;
}
for(int i = 0; i <= m; i ++) {
b0[i] = B[i] / M;
b1[i] = B[i] % M;
//cout << b1[i].x << endl;
}
DFT(a1, b0); DFT(a0, b1);
for(int i = 0; i < lim; i ++) {
p[i] = a0[i] * b0[i] + I * a1[i] * b0[i];
q[i] = a0[i] * b1[i] + I * a1[i] * b1[i];
//cout << (LL) b0[i].y << endl;
}
FFT(p, -1); FFT(q, -1);
for(int i = 0; i <= n + m; i ++) {
C[i] = ( (LL) M * M % P * qz(p[i].x) % P + (LL) M * qz(p[i].y) % P + (LL) M * qz(q[i].x) % P + qz(q[i].y) ) % P;
}
}

void getinv(int *F, int *G, int dep) {
if(dep == 1) {
G[0] = power(F[0], P - 2);
return ;
}
getinv(F, G, (dep + 1) >> 1);

static int C[N], D[N];

for(int i = 0; i < dep; i ++) C[i] = F[i];
for(int i = dep; i < (dep << 1); i ++) C[i] = 0;
for(int i = 0; i < (dep << 1); i ++) D[i] = 0;

//for(int i = 0; i < dep; i ++) cout << D[i] << ' '; cout << endl;
//cout << "-----------------------------------------" << endl;
//for(int i = 0; i <= dep; i ++) cout << C[i] << ' '; cout << endl;
//for(int i = 0; i <= dep; i ++) cout << G[i] << ' '; cout << endl;
MTT(C, G, D, dep, dep);
//for(int i = 0; i <= dep; i ++) cout << D[i] << ' ' ; cout << endl;
//cout << "-----------------------------------------" << endl;
//lfor(int i = 0; i < dep; i ++) cout << D[i] << ' '; cout << endl;
for(int i = dep; i < (dep << 1); i ++) D[i] = 0;
//for(int i = 0; i < (dep << 2); i ++) C[i] = 0;
//cout << "-----------------------------------------" << endl;
//for(int i = 0; i <= dep; i ++) cout << D[i] << ' '; cout << endl;
//for(int i = 0; i <= dep; i ++) cout << G[i] << ' '; cout << endl;

MTT(D, G, C, dep, dep);

//for(int i = 0; i <= dep; i ++) cout << C[i] << ' ' ; cout << endl;
//cout << "----------------------------------------" << endl;

for(int i = dep; i < (dep << 1); i ++) C[i] = 0;
for(int i = 0; i < dep; i ++) {
int v = (2LL * G[i] % P - C[i] + P) % P;
G[i] = v;
}
for(int i = dep; i < (dep << 1); i ++) G[i] = 0;
//cout << "----------------------------------------" << endl;
//for(int i = 0; i <= dep; i ++) cout << G[i] << ' '; cout << endl;
//cout << "----------------------------------------" << endl;

}

int n, m;
int a[N], b[N], c[N];

int main() {
//freopen("a.in", "r", stdin);
//freopen("a.out", "w", stdout);
n = read(); P = 1e9 + 7;
for(int i = 0; i < n; i ++) a[i] = read() % P;
getinv(a, b, n);
for(int i = 0; i < n; i ++) printf("%d ", b[i]);
return 0;
}

三模NTT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <bits/stdc++.h>
using namespace std;

#define LL long long

inline int read() {
int x = 0, f = 1; char a = getchar();
for(; a > '9' || a < '0'; a = getchar()) if(a == '-') f = -1;
for(; a <= '9' && a >= '0'; a = getchar()) x = x * 10 + a - '0';
return x * f;
}

namespace Data {
LL power(LL x, LL k, LL P) {
//cout << k << ' ' << P << endl;
LL res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
} return res;
}
const int A = 998244353, B = 1004535809, C = 469762049;
//const int P = 1e9 + 7;
const int G = 3;
const LL I1 = power(A, B - 2, B);
const LL AB = (LL)A * B;
const LL I2 = power(AB % C, C - 2, C);
class Int {
private :
LL a, b, c;
public :
Int() {}
Int(int V) : a(V % A), b(V % B), c(V % C) {}
Int(int X, int Y, int Z) : a(X), b(Y), c(Z) {}
static inline Int reduce(const Int &x) {
return Int ( (x.a % A + A) % A, (x.b % B + B) % B, (x.c % C + C) % C );
}
inline friend Int operator +(const Int &x, const Int &y) {
return reduce( Int (x.a + y.a, x.b + y.b, x.c + y.c) );
}
inline friend Int operator -(const Int &x, const Int &y) {
return reduce( Int (x.a - y.a + A, x.b - y.b + B, x.c - y.c + C) );
}
inline friend Int operator *(const Int &x, const Int &y) {
return reduce( Int (x.a * y.a % A, x.b * y.b % B, x.c * y.c % C) );
}
inline LL ask(int P) {
LL x = (b - a + B) % B * I1 % B * A + a;
return ((LL) (c - x % C + C) % C * I2 % C * ( AB % P ) % P + x) % P;
}
};
}

using Data :: Int;

const int N = 4e5 + 10;

int lim, bit;
int rev[N];
void init(int n) {
lim = 1; bit = 0;
while(lim < n) lim <<= 1, bit ++;
for(int i = 0; i < lim; i ++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
}

void NTT(Int *A, int type) {
for(int i = 0; i < lim; i ++) if(i < rev[i]) swap(A[i], A[rev[i]]);
for(int dep = 1; dep < lim; dep <<= 1) {
Int Wn = ((type == 1) ?
Int(
Data :: power(3, (Data :: A - 1) / (dep << 1), Data :: A),
Data :: power(3, (Data :: B - 1) / (dep << 1), Data :: B),
Data :: power(3, (Data :: C - 1) / (dep << 1), Data :: C) ) :
Int(
Data :: power(Data :: power(3, Data :: A - 2, Data :: A), (Data :: A - 1) / (dep << 1), Data :: A),
Data :: power(Data :: power(3, Data :: B - 2, Data :: B), (Data :: B - 1) / (dep << 1), Data :: B),
Data :: power(Data :: power(3, Data :: C - 2, Data :: C), (Data :: C - 1) / (dep << 1), Data :: C) ) );
//cout << Wn.a << ' ' << Wn.b << ' ' << Wn.c << endl;
for(int j = 0, len = (dep << 1); j < lim; j += len) {
Int w(1, 1, 1);
for(int k = 0; k < dep; k ++, w = w * Wn) {
Int x = A[j + k], y = w * A[j + k + dep];
A[j + k] = x + y;
A[j + k + dep] = x - y;
}
}
}
if(type == -1) {
Int inv (Data :: power(lim, Data :: A - 2, Data :: A),
Data :: power(lim, Data :: B - 2, Data :: B),
Data :: power(lim, Data :: C - 2, Data :: C));
for(int i = 0; i < lim; i ++) A[i] = A[i] * inv;
}
}

int n, m, P;
Int f[N], g[N];

signed main() {
#ifdef IN
freopen("P4245_11.in", "r", stdin);
freopen("a.out", "w", stdout);
#endif
n = read(); m = read(); P = read();
for(int i = 0; i <= n; i ++) f[i] = Int (read() % P);
for(int i = 0; i <= m; i ++) g[i] = Int (read() % P);
init(n + m + 1);
NTT(f, 1); NTT(g, 1);
for(int i = 0; i < lim; i ++) f[i] = f[i] * g[i];
NTT(f, -1);
for(int i = 0; i <= n + m; i ++) printf("%lld ", f[i].ask(P));
return 0;
}

常系数齐次线性递推 & BM算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183

#include <bits/stdc++.h>

using namespace std;

#define LL long long

namespace BerlekampMassey {
const int P = 998244353;
const int N = 5e3 + 10;
inline int Mod(int x) { return x + ((x >> 31) & P); }
int power(int x, int k) { x = (x % P + P) % P;
int res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
}
return res;
}
pair<int, int> getfac(int q, int A = 10000) {
int x = q, y = P, a = 1, b = 0;
while (x > A) {
swap(x, y); swap(a, b);
a -= x / y * b;
x %= y;
}
return make_pair(x, a);
}
vector<int> solve(int *A, int n) {
vector<int> ans, lst;
int w = 0, delta = 0;
for(int i = 1; i <= n; i ++) {
int tmp = 0;
for(int j = 0; j < ans.size(); j ++)
tmp = (tmp + (LL) A[i - j - 1] * ans[j] % P) % P;
if((A[i] - tmp) % P == 0) continue;
if(! w) {
w = i;
delta = A[i] - tmp; delta = (delta % P + P) % P;
for(int j = i; j; j --) ans.push_back(0);
continue;
}
vector<int> now = ans;
int mul = (LL) (A[i] - tmp + P) * power(delta, P - 2) % P;
if(ans.size() < lst.size() + i - w) ans.resize(lst.size() + i - w);
ans[i - w - 1] = (ans[i - w - 1] + mul) % P;
for(int j = 0; j < lst.size(); j ++)
ans[i - w + j] = (ans[i - w + j] - (LL) mul * lst[j] % P + P) % P;
if(now.size() - i < lst.size() - w) {
lst = now; w = i; delta = A[i] - tmp;
}
}
return ans;
}
}

const int N = 1e6 + 10;
const int P = 998244353;
inline int Mod(int x) { return x + ((x >> 31) & P); }

int power(int x, int k) {
int res = 1;
while(k) {
if(k & 1) res = (LL) res * x % P;
x = (LL) x * x % P; k >>= 1;
}
return res;
}

struct Poly {
vector<int> vec;
Poly(int SZ = 0) { vec.resize(SZ); }
void resize(int len) {
while(vec.size() < len) vec.push_back(0);
while(vec.size() > len) vec.pop_back();
}
int size() {
return vec.size();
}
int & operator [] (int id) { return vec[id]; }
int ask(int v) {
int ans = 0;
for(int i = vec.size() - 1; i >= 0; i --)
ans = (vec[i] + (LL) v * ans % P) % P;
return ans;
}
void deri();
void inte();
void reverse();
} ;

void Poly :: reverse() {
for(int i = 0; i < vec.size() - i; i ++) swap(vec[i], vec[vec.size() - i - 1]);
}

inline Poly operator +(Poly x, Poly y) {
Poly z;
if(x.size() < y.size()) std :: swap(x, y);
z = x;
for(int i = 0; i < y.size(); i ++)
z[i] = Mod(z[i] + y[i] - P);
return z;
}

inline Poly operator -(Poly x, Poly y) {
Poly z = x;
if(z.size() < y.size()) z.resize(y.size());
for(int i = 0; i < y.size(); i ++) z[i] = Mod(z[i] - y[i]);
return z;
}

inline Poly operator *(Poly x, Poly y) {
Poly z(x.size() + y.size() - 1);
for(int i = 0; i < x.size(); i ++)
for(int j = 0; j < y.size(); j ++)
z[i + j] = Mod(z[i + j] + (LL) x[i] * y[j] % P - P);
return z;
}

inline Poly operator ~(Poly f) {
Poly g(f.size());
g[0] = power(f[0], P - 2);
for(int n = 1; n < f.size(); n ++) {
int res = 0;
for(int i = 0; i < n; i ++) res = Mod(res + (LL) g[i] * f[n - i] % P - P);
g[n] = P - (LL) g[0] * res % P;
}
return g;
}

Poly operator /(Poly F, Poly G) {
int n = F.size() - 1;
int m = G.size() - 1;
Poly RF = F, RG = G;
RF.reverse(); RF.resize(n - m + 1);
RG.reverse(); RG.resize(n - m + 1);
Poly IRG = ~ RG, Q = RF * IRG;
Q.resize(n - m + 1); Q.reverse();
Q.resize(n - m + 1);
return Q;
}

Poly operator %(Poly F, Poly G) {
if(F.size() < G.size()) return F;
Poly Q = F / G;
Poly R = F - Q * G;
R.resize(G.size() - 1);
return R;
}

int recursion(vector<int> &p, vector<int> &f, LL n) {
Poly pl(p.size() + 1);
pl[p.size()] = 1;
for(int i = 0; i < p.size(); i ++)
pl[i] = P - p[p.size() - i - 1];
Poly x(2), res(1);
x[1] = 1;
res[0] = 1;
while(n) {
if(n & 1) res = res * x % pl;
x = x * x % pl; n >>= 1;
}
int ans = 0;
for(int i = 0; i < f.size(); i ++) ans = Mod(ans + (LL) res[i] * f[i] % P - P);
return ans;
}

int n, m;
int A[N];

int main() {
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> A[i];
vector<int> p, f;
p = BerlekampMassey :: solve(A, n);
for(int v : p) cout << v << ' '; cout << endl;
f.resize(p.size());
for(int i = 0; i < p.size(); i ++) f[i] = A[i + 1];
int ans = recursion(p, f, m);
cout << ans << endl;
return 0;
}

字符串

KMP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scanf("%s\n%s", s1 + 1, s2 + 1);
n = strlen(s1 + 1);
m = strlen(s2 + 1);

for (int i = 2, j = 0; i <= m; i ++) {
while(j && s2[j + 1] != s2[i]) j = p[j];
if(s2[j + 1] == s2[i]) j ++;
p[i] = j;
}
for(int i = 1, j = 0; i <= n; i ++) {
while(j && s1[i] != s2[j + 1]) j = p[j];
if(s2[j + 1] == s1[i]) j ++;
if(j == m) {
printf("%d\n", i - m + 1);
j = p[j];
}
}
lep (i, 1, m) printf("%d%c", p[i], " \n"[i == m]);

最小表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;

#define endl '\n'

const int N = 6e5 + 10;

int n, ans;
int s[N];

int main() {
std :: ios :: sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) cin >> s[i], s[i + n] = s[i];

for(int i = 1; i <= n; ) {
int j = i, k = i + 1;
while(k <= 2 * n && s[j] <= s[k]) {
if(s[j] < s[k]) j = i;
else j ++;
k ++;
}
while(i <= j) {
//ans ^= i + k - j - 1;
i += k - j;
if(i <= n) ans = i;
}
}

for(int i = 1; i <= n; i ++) cout << s[ans - 1 + i] << " \n"[i == n];
return 0;
}

自动机相关

基本定义与约定:

  • 称字符串 $T$ 匹配 $S$ 为 $T$ 在 $S$ 中出现。
  • 模式串:相当于题目给出的 字典,用于匹配的字符串。下文也称 单词
  • 文本串:被匹配的字符串。
  • 更多约定见 常见字符串算法。

AC 自动机 ACAM


前置知识:字典树,KMP 算法与 动态规划 思想。

AC 自动机是一类确定有限状态自动机,这说明它有完整的 DFA 五要素,分别是起点 $s$(Trie 树根节点),状态集合 $Q$(Trie 树上所有节点),接受状态集合 $F$(所有以某个单词作为后缀的节点),字符集 $\Sigma$(题目给定)和转移函数 $\delta$(类似 KMP 求解)。

AC 自动机全称 Aho-Corasick Automaton,简称 ACAM。它的用途非常广泛,是重要的字符串算法($8$ 级)。

1.1 算法详解

AC 自动机用于解决 多模式串 匹配问题:给定 字典 $s$ 和文本串 $t$,求每个单词 $s_i$ 在 $t$ 中出现的次数。当然,它的实际应用十分广泛,远超这一基本问题。ACAM 与 KMP 的不同点在于后者仅有一个模式串,而前者有多个。

朴素的基于 KMP 的暴力时间复杂度为 $|t|\times N + \sum |s_i|$,其中 $N$ 是单词个数。因为进行一次匹配的时间复杂度为 $|s_i| + |t|$。当单词数量 $N$ 较大时,无法接受。

多串问题自然首先考虑建出字典树。根据其定义,字典树上任意节点 $q\in Q$ 与所有单词的某个前缀 一一对应。设节点(节点也称状态)$i$ 表示的字符串为 $t_i$。

借鉴 KMP 算法的思想,我们考虑对于每个状态 $q$,求出其 失配指针 $fail_q$。类似 KMP 的失配数组 $nxt$,失配指针的含义为:$q$ 所表示字符串 $t_q$ 的 最长真后缀 $t_q[j, |t_q|]\ (2\leq j\leq |t_q| + 1)$,使得该后缀作为某个单词的前缀出现。这说明 $t_q[j, |t_q|]$ 恰好对应了字典树上某个状态,因此一个状态的失配指针指向另一个长度比它短的状态。注意,这样的后缀 可能不存在,因此失配指针可能指向表示空串的根节点。

从 $q$ 向字符串 $fail_q$ 连一条有向边,就得到了 ACAM 的 fail 树

  • 例如,当 $s = {\texttt{b},\ \texttt{ab}}$ 时,$\tt ab$ 会向 $\tt b$ 连边,因为 $\tt ab$ 最长的(也是唯一的)在 $s_i$ 中作为前缀出现的后缀为 $\tt b$。
  • 再例如,当 $s = {\texttt{aba},\ \texttt {baba}}$ 时,$\tt ab$ 会向 $\tt b$ 连边, $\tt bab$ 会向 $\tt ab$ 连边,$\tt aba$ 会向 $\tt ba$ 连边,而 $\tt baba$ 会向 $\tt aba$ 连边。对于每一条有向边 $q \to fail_q$,后者是前者的后缀,也是 $s_i$ 的前缀。

考虑用类似 KMP 的算法求解失配指针:首先令 $fail_q\gets fail_{fa_q}$。若当前的 $fail_q$ 没有 $fa_q\to q$ 这条(字典树上的)边所表示的字符 $c$ 的转移,则令 $fail_q\gets fail_{fail_q}$,否则 $fail_q = \mathrm{trans}(fail_q, c)$,即字典树上在 $fail_q$ 处添加字符 $c$ 后到达的状态。若 $fail_q$ 已经指向根,但还是没找到出边,则 $fail_q$ 最终就指向根。

失配指针已经足够强大,但这并不是 AC 自动机的完全体。我们尝试将每个状态的所有字符转移 $\delta(i, c)$ 都封闭在状态集合 $Q$ 里面。把 KMP 自动机的转移拎出来观察

$$\delta(i, c) = \begin{cases} i+1 & s_{i + 1} = c \ 0 & s_{i + 1} \neq c \land i = 0 \ \delta(nxt_i, c) & s_{i + 1} \neq c \land i \neq 0 \ \end{cases}$$

设字典树的根为节点 $0$,AC 自动机的转移可类似地写为:

$$\delta(i,c) = \begin{cases} \mathrm{trans}(i, c) & \mathrm{if}\ \mathrm{trans}(i, c)\ \mathrm{exist} \ 0 & \mathrm{if}\ \mathrm{trans}(i, c)\ \mathrm{doesn’t\ exist} \land i = 0\ (\mathrm{which\ is \ root}) \ \delta(fail_i, c) & \mathrm{if}\ \mathrm{trans}(i, c)\ \mathrm{doesn’t\ exist} \land i \neq 0 \end{cases}$$

$\delta(i,c)$ 表示往状态 $i$ 后面添加字符 $c$,所得字符串的 最长的 与 $s_i$ 前缀 匹配的 后缀 所表示的状态。也可理解为从 $i$ 开始跳 $fail$ 指针,遇到的第一个有字符 $c$ 的转移对应转移到的节点:若 $i$ 本身有转移,则 $\delta(i, c)$ 就等于 $\mathrm{trans}(i, c)$,否则向上跳一层 $fail$ 指针,等于 $\delta(fail_i, c)$。

根据已有信息递推,这是 动态规划 的核心思想。即求解 $\delta$ 函数的的过程本质上是一类 DP。

当 $\mathrm{trans}(i, c)$ 存在时,设其为 $q$, 则有 $fail_q = \delta(fail_i, c)$。因为根据求 $fail_q$ 的方法,我们会先令 $fail_q \gets fail_i$,然后跳到第一个有字符 $c$ 的位置,令 $fail_q$ 等于该位置添加 $c$ 转移到的状态。这和 $\delta(fail_i, c)$ 的定义等价。

有了这一性质,我们就不需要预先求出失配指针,而是在建造 AC 自动机的同时一并求出。由于我们需要保证在计算一个状态的转移时,其失配指针指向的状态的转移已经计算完毕,又因为失配指针长度小于原串长度,故使用 BFS 建立 AC 自动机。一般形式的 AC 自动机代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int node, son[N][S], fa[N];

void ins(string s) { // 建出 trie 树

int p = 0;

for(char it : s) {

if(!son[p][it - 'a']) son[p][it - 'a'] = ++node;

p = son[p][it - 'a'];

}

}

void build() { // 建出 AC 自动机

queue <int> q;

for(int i = 0; i < S; i++) if(son[0][i]) q.push(son[0][i]); // 对于第一层特判,因为 fa[0] = 0,此处即转移的第二种情况

while(!q.empty()) { // 求得的 son[t][i] 就是文章中的转移函数 delta(t, i),相当于合并了 trie 和 AC 自动机的转移函数

int t = q.front(); q.pop();

for(int i = 0; i < S; i++)

if(son[t][i]) fa[son[t][i]] = son[fa[t]][i], q.push(son[t][i]); // 转移的第一种情况:原 trie 图有 trans(t, i) 的转移

else son[t][i] = son[fa[t]][i]; // 转移的第三种情况

}

}

特别的,在 ACAM 上会有一些 终止节点 $p$,代表一个单词或以一个单词结尾,即 $p$ 对应的字符串 $t_p$ 的某个 后缀 在字典 $s$ 中作为 单词 出现。 若状态 $p$ 本身表示一个单词,即 $t_p\in s$,则称为 单词节点。所有终止节点 $p$ 对应着 DFA 的 接受状态集合 $F$:ACAM 接受且仅接受以给定词典中的某一个单词结尾的字符串。

总结一下我们使用到的约定和定义:

  • 节点也被称为 状态
  • 设字典树上状态 $i$ 所表示的字符串为 $t_i$。
  • 失配指针 $fail_q$ 的含义为 $q$ 所表示字符串 $t_q$ 的最长真后缀 $t_q[j, |t_q|]\ (2\leq j\leq |t_q| + 1)$ 使得该后缀作为某个单词的前缀出现。
  • $\delta(i,c)$ 表示往状态 $i$ 后添加字符 $c$,所得字符串的 最长的 与某个单词的 前缀 匹配的 后缀 所表示的状态。它也是从 $i$ 开始,不断跳失配指针直到遇到一个有字符 $c$ 转移的状态 $p$,添加字符 $c$ 后得到的状态 $\mathrm{trans}(p, c)$。
  • 终止节点 $p$ 代表一个单词,或以一个单词结尾。
  • 所有终止节点 $p$ 组成的集合对应着 DFA 的 接受状态集合 $F$。
  • 若状态 $p$ 本身表示一个单词,即 $t_p\in s$,则称为 单词节点

1.2 fail 树的性质与应用

AC 自动机的核心就在于 fail 树。它有非常好的性质,能够帮我们解决很多问题。

  • 性质 0:它是一棵 有根树,支持树剖,时间戳拍平,求 LCA 等各种树上路径或子树操作。
  • 性质 1:对于节点 $p$ 及其对应字符串 $t_p$,对于其子树内部所有节点 $q\in \mathrm{subtree}(p)$,都有 $t_p$ 是 $t_q$ 的后缀,且 $t_p$ 是 $t_q$ 的后缀 当且仅当 $q\in \mathrm{subtree}(p)$。根据失配指针的定义易证。
  • 性质 2:若 $p$ 是终止节点,则 $p$ 的子树全部都是终止节点。根据 fail 指针的定义,容易发现对于在 fail 树上具有祖先 - 后代关系的点对 $p,q$,$t_p$ 是 $t_q$ 的 Border,这意味着 $t_p$ 是 $t_q$ 的后缀。因此,若 $t_p$ 以某个单词结尾,则 $t_q$ 也一定以该单词结尾,得证。
  • 性质 3:定义 $ed_p$ 表示作为 $t_p$ 后缀的单词数量。若单词互不相同,则 $ed_p$ 等于 fail 树从 $p$ 到根节点上单词节点的数量。若单词可以重复,则 $ed_p$ 等于这些单词节点所对应的单词的出现次数之和。
  • 常用结论:一个单词在匹配串 $S$ 中出现次数之和,等于它在 $S$ 的 所有前缀中作为后缀出现 的次数之和。

根据性质 3,有这样一类问题:单词有带修权值,多次询问对于某个给定的字符串 $S$,所有单词的权值乘以其在 $S$ 中出现次数之和。根据常用结论,问题初步转化为 fail 树上带修点权,并对于 $S$ 的每个前缀,查询该前缀所表示的状态到根的权值之和。

通常带修链求和要用到树剖,但查询具有特殊性质:一个端点是根。因此,与其单点修改链求和,不如 子树修改单点查询。实时维护每个节点的答案,这样修改一个点相当于更新子树,而查询时只需查单点。转化之前的问题需要树剖 + 数据结构 $\log ^ 2$ 维护,但转化后即可时间戳拍平 + 树状数组单 $\log$ 小常数解决。

补充:对于普通的链求和,只需差分转化为三个到根链求和也可以使用上述技巧。链加,单点查询 也可以通过转化变成 单点加,子树求和。只要包含一个单点操作,一个链操作,均可以将链操作转化为子树操作,即可将时间复杂度更大的树剖 BIT 换成普通 BIT。

  • 性质 4:把字符串 $t$ 放在字典 $s$ 的 AC 自动机上跑,得到的状态为 $t$ 的最长后缀,满足它是 $s$ 的前缀。

1.3 应用

大部分时候,我们借助 ACAM 刻画多模式串的匹配关系,求出文本串与字典的 最长匹配后缀。但 ACAM 也可以和动态规划结合:在利用动态规划思想构建的自动机上进行 DP,这是 DP 自动机 算法。

1.3.1 结合动态规划

ACAM 除了能够进行字符串匹配,还常与动态规划相结合,因为它精确刻画了文本串与 所有 模式串的匹配情况。同时,$\delta$ 函数自然地为动态规划的转移指明了方向。因此,当遇到形如 “不能出现若干单词” 的字符串 计数或最优化 问题,可以考虑在 ACAM 上 DP,将 ACAM 的状态写进 DP 的一个维度。

例如非常经典的 [JSOI2007] 文本生成器。题目要求至少包含一个单词,补集转化相当于求 不包含任何一个单词 的长为 $m$ 的字符串数量。考虑到我们只关心当前字符串的长度,和它与所有单词的匹配情况,设 $f_{i,j}$ 表示长为 $i$ 且放到所有单词建出的 ACAM 上能够转移到状态 $j$ 的字符串数量。转移即枚举下一个字符 $c$ 是什么,$f_{i,j}\to f_{i+1,\delta(j,c)}$。根据限制,需要保证 $j$ 和 $\delta(j,c)$ 都不是终止节点,最终答案即 $26^m-\sum_{\ q\in Q\land q\notin F} f_{m, q}$。时间复杂度 $\mathcal{O}(nm|\Sigma||s_i|)$。

具体转移方式视题目而定。矩阵乘法也可以是广义矩阵乘法,如例 XII.

1.4 注意点

  • 建出字典树后不要忘记调用 build 建出 ACAM。
  • 注意模式串是否可以重复。
  • 在构建 ACAM 的过程中,不要忘记递推每个节点需要的信息。如 $ed_p$ 由 $ed_{fa_p}$ 和状态 $p$ 所表示的单词数量相加得到。

1.5 例题

I. P3808 【模板】AC 自动机(简单版)

本题相同编号的串多次出现仅算一次,因此题目相当于求:文本串 $t$ 在模式串 $s_i$ 建出的 ACAM 上匹配时经过的所有节点到根的路径的并上单词节点的个数。

设当前状态为 $p$,每次跳 $p$ 的失配指针,加上经过节点表示的单词个数(单词可能相同)并标记,直到遇到标记节点 $q$,说明 $q$ 到根都已经被考虑到。注意上述过程并不改变 $p$ 本身。时间复杂度线性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <bits/stdc++.h>

using namespace std;



const int N = 1e6 + 5;

const int S = 26;

int n, node, son[N][S], fa[N], ed[N];

string s;

void ins(string s) {

int p = 0;

for(char it : s) {

if(!son[p][it - 'a']) son[p][it - 'a'] = ++node;

p = son[p][it - 'a'];

} ed[p]++;

}

void build() {

queue <int> q;

for(int i = 0; i < S; i++) if(son[0][i]) q.push(son[0][i]);

while(!q.empty()) {

int t = q.front(); q.pop();

for(int i = 0; i < S; i++)

if(son[t][i]) fa[son[t][i]] = son[fa[t]][i], q.push(son[t][i]);

else son[t][i] = son[fa[t]][i];

}

}

int main() {

cin >> n;

for(int i = 1; i <= n; i++) cin >> s, ins(s);

int p = 0, ans = 0; cin >> s, build();

for(char it : s) {

int tmp = p = son[p][it - 'a'];

while(ed[tmp] != -1) ans += ed[tmp], ed[tmp] = -1, tmp = fa[tmp];

} cout << ans << endl;

return 0;

}

后缀自动机 SAM

后缀自动机全称 Suffix Automaton,简称 SAM,是一类极其有用但难以真正理解的字符串后缀结构($10$ 级)。它是笔者一年以前学习的算法,现在进行复习并重构学习笔记,看看能不能悟到一些新的东西。

2.1 基本定义与引理

SAM 相关的定义非常多,需要牢记并充分理解它们,否则学习 SAM 会非常吃力,因为符号化的语言相较于直观的图片和实例更难以理解。

首先,我们给出 SAM 的定义:一个长为 $n$ 的字符串 $s$ 的 SAM 是一个接受 $s$ 的所有 后缀最小 的有限状态自动机。具体地,SAM 有 状态集合 $Q$,每个状态是有向无环图上的一个节点。从每个状态出发有若干条或零条 转移边,每条转移边都 对应一个字符(因此,一条路径表示一个 字符串),且从一个状态出发的转移互不相同。根据 DFA 的定义,SAM 还存在 终止状态集合 $F$,表示从初始状态 $T$ 到任意终止状态的任意一条路径与 $s$ 的一个 后缀 一一对应。

SAM 最重要,也是最基本的一个性质:从 $T$ 到任意状态的所有路径与 $s$ 的 所有 子串 一一对应。我们称状态 $p$ 表示字符串 $t_p$,当且仅当存在一条 $T\to p$ 的路径使得该路径所表示的字符串为 $t_p$。根据上述性质,$t_p$ 是 $s$ 的子串。

  • 定义转移边 $p\to q$ 表示的字符为 $c_{p, q}$。

  • 定义 $\delta(p, c)$ 表示状态 $p$ 添加字符 $c$ 转移到的状态。

  • 定义 前缀 状态集合 $P$ 由所有前缀 $s[1, i]$ 对应的状态组成。

  • SAM 的有向无环转移图也是有向无环单词图(DAWG, Directed Acyclic Word Graph)。

  • $\mathrm{endpos}(t)$:字符串 $t$ 在 $s$ 中所有出现的 结束位置集合。例如,当 $s = \texttt{“abcab”}$ 时,$\mathrm{endpos}(\texttt{“ab”}) = {2, 5}$,因为 $s[1 : 2] = s[4 : 5] = \texttt{“ab”}$。

  • $\mathrm{substr}(p)$:状态 $p$ 所表示的所有子串的 集合

  • $\mathrm{shortest}(p)$:状态 $p$ 所表示的所有子串中,长度 最短 的那一个子串。

  • $\mathrm{longest}(p)$:状态 $p$ 所表示的所有子串中,长度 最长 的那一个子串。

  • $\mathrm{minlen}(p)$:状态 $p$ 所表示的所有子串中,长度 最短 的那一个子串的 长度。$\mathrm{minlen}(i) = |\mathrm{shortest}(i)|$。

  • $\mathrm{len}(i)$:状态 $p$ 所表示的所有子串中,长度 最长 的那一个子串的 长度。$\mathrm{len}(i)=|\mathrm{longest}(i)|$。

两个字符串 $t_1, t_2$ 的 $\mathrm{endpos}$ 可能相等。例如当 $s = \texttt{“abab”}$ 时,$\mathrm{endpos}(\texttt{“b”}) = \mathrm{endpos}(\texttt{“ab”})$。这样,我们可以将 $s$ 的子串划分为若干 等价类,用一个状态表示。SAM 的每个状态对应若干 $\mathrm{endpos}$ 集合相同的子串。换句话说,$\forall t\in \mathrm{substr}(p)$,$\mathrm{endpos}(t)$ 相等。因此,SAM 的状态数等于所有子串的等价类个数(初始状态对应空串)。

读者应该有这样的直观印象:SAM 的每个状态 $p$ 都表示一个独一无二的 $\mathrm{endpos}$ 等价类,它对应着在 $s$ 中出现位置相同的一些子串 $\mathrm{substr}(p)$。$\mathrm{shortest}(p),\mathrm{longest}(p),\mathrm{minlen}(p)$ 和 $\mathrm{len}(p)$ 描述了 $\mathrm{substr}(p)$ 最短和最长的子串及其长度。

转移边与 $\mathrm{substr}$ 的联系:任意一条 $T\to p$ 的路径 $P$ 所表示的字符串 $t_{P}\in \mathrm{substr}(p)$。

在引出 SAM 的核心定义「后缀链接」前,我们需要证明关于上述概念的一些性质。下列引理的内容部分来自 OI-wiki,相关链接见 Part 2.4.

引理 1:考虑两个非空子串 $u$ 和 $w$(假设 $|u|\leq |w|$)。要么 $\mathrm{endpos}(u)\cup \mathrm{endpos}(w)=\varnothing$,要么 $\mathrm{endpos}(w) \subseteq \mathrm{endpos}(u)$,取决于 $u$ 是否为 $w$ 的一个后缀:

$$\begin{cases} \mathrm{endpos}(w) \subseteq \mathrm{endpos}(u) & \mathrm{if} \ u\ \mathrm{is\ a\ suffix\ of}\ w \ \mathrm{endpos}(u) \cup \mathrm{endpos}(w) = \varnothing & \mathrm{otherwise} \end{cases}$$

证明:若存在位置 $i$ 满足 $i\in \mathrm{endpos}(u)$ 且 $i\in \mathrm{endpos}(w)$,说明 $u$ 和 $w$ 以 $i$ 为结束位置在 $s$ 中出现。由于 $|u|\leq |w|$,所以 $u$ 必然是 $w$ 的后缀,因此 $w$ 出现的位置 $u$ 必然以 $w$ 的后缀形式出现,即对于任意 $i\in \mathrm{endpos}(w)$ 有 $i\in \mathrm{endpos}(u)$。否则不存在这样的位置 $i$,即 $\mathrm{endpos}(u) \cup \mathrm{endpos}(w) = \varnothing$。

引理 2:考虑一个状态 $p$。$p$ 所表示的所有子串长度连续,且 较短者总是较长者的后缀

证明:根据引理 1,若两个子串 $\mathrm{endpos}$ 相同(这也说明它们属于相同状态),则较短者总是较长者的后缀,后半部分得证。

对于前半部分考虑反证:假设 $\mathrm{longest}(p)$ 长为 $L\ (\mathrm{minlen}(p) < L < \mathrm{len}(p))$ 的后缀 $t_L\notin \mathrm{substr}(p)$。由于 $t_L$ 是 $\mathrm{longest}(p)$ 的 真后缀,故 $\mathrm{endpos}(\mathrm{longest}(p)) \subseteq \mathrm{endpos}(t_L)$。根据假设,$\mathrm{endpos}(\mathrm{longest}(p)) \neq \mathrm{endpos}(t_L)$。又因为 $\mathrm{shortest}(p)$ 是 $t_L$ 的 真后缀,故 $\mathrm{endpos}(t_L) \subseteq \mathrm{endpos}(\mathrm{shortest}(p))$,因此 $|\mathrm{endpos}(\mathrm{longest}(p))| < |\mathrm{endpos}(t_L)| \leq |\mathrm{endpos}(\mathrm{shortest}(p))|$,这与 $\mathrm{endpos}(\mathrm{longest}(p)) = \mathrm{endpos}(\mathrm{shortest}(p))$ 矛盾,证毕。

简单地说,对于一个子串 $t$ 的所有后缀,其 $\mathrm{endpos}$ 集合大小随着后缀长度减小而单调不降。这很好理解:后缀越长,在 $s$ 中出现的位置就越少

推论 1:对于子串 $t$ 的所有后缀,其 $\mathrm{endpos}$ 集合大小随后缀长度减小而单调不降,且 较小的 $\mathrm{endpos}$ 集合包含于较大的 $\mathrm{endpos}$ 集合

引理 2 是非常重要的性质。有了它,我们就可以定义后缀链接了。

  • 定义状态 $p$ 的 后缀链接 $\mathrm{link}(p)$ 指向 $\mathrm{longest}(p)$ 最长 的一个后缀 $w$ 满足 $w\notin \mathrm{substr}(p)$ 所在的状态。换句话说,一个后缀链接 $\mathrm{link}(p)$ 连接到对应于 $\mathrm{longest}(p)$ 最长的处于另一个 $\mathrm{endpos}$ 等价类的后缀所在的状态。根据引理 2,$\mathrm{minlen}(i) = \mathrm{len(link}(i))+1$。

引理 3:所有后缀链接形成一棵以 $T$ 为根的树。

证明:对于任意不等于 $T$ 的状态,沿着后缀链接移动总能达到一个所表示字符串更短的状态,直到 $T$。

  • 定义 后缀路径 $p\to q$ 表示在后缀链接形成的树上 $p\to q$ 的路径。

引理 4:通过 $\mathrm{endpos}$ 集合构造的树(每个子节点的 $\mathrm {subset}$ 都包含在父节点的 $\mathrm{subset}$ 中)与通过后缀链接 $\mathrm{link}$ 构造的树相同。

根据推论 1 与后缀链接的定义容易证明。因此,后缀链接构成的树本质上是 $\mathrm{endpos}$ 集合构成的一棵树。

上图图源 OI-wiki。我们给出每个状态的 $\mathrm{endpos}$ 集合以便更好理解引理 4:$\mathrm{endpos}(\texttt{“a”}) = {1}$,

$$\begin{aligned} \mathrm{endpos}(\texttt{“ab”}) = {2} \ \mathrm{endpos}(\texttt{“abcb”, “bcb”, “cb”}) = {4} \ \end{aligned} \subsetneq \mathrm{endpos}(\texttt{“b”}) = {2, 4} \$$

$$\begin{aligned} \mathrm{endpos}(\texttt{“abc”}) = {3} \ \mathrm{endpos}(\texttt{“abcbc”, “bcbc”, “cbc”}) = {5} \ \end{aligned} \subsetneq \mathrm{endpos}(\texttt{“bc”, “c”}) = {3, 5} \$$

2.2 关键结论

我们还需要以下定理确保构建 SAM 的算法的正确性,并使读者对上述定义形成感性的直观的认知。

结论 1.1:从任意状态 $p$ 出发跳后缀链接到 $T$ 的路径,所有状态 $q\in p\to T$ 的 $[\mathrm{minlen}(q),\mathrm{len}(q)]$ 不交,单调递减且并集形成 连续 区间 $[0,\mathrm{len}(p)]$。

证明:根据后缀链接的性质 $\mathrm{len}(\mathrm{link}(p)) + 1 = \mathrm{minlen}(p)$ 即证。

结论 1.2:从任意状态 $p$ 出发跳后缀链接到 $T$ 的路径,所有状态 $q\in p\to T$ 的 $\mathrm{substr}(q)$ 的并集为 $\mathrm{longest}(p)$ 的 所有后缀

证明:由结论 1.1 和后缀链接的定义易证。

结论 2.1:$\forall t_p\in \mathrm{substr}(p)$,若存在 $p\to q$ 的 转移边,则 $t_p + c_{p,q}\in \mathrm{substr}(q)$。

证明:根据 $\mathrm{substr}$ 的定义可得。

结论 2.2:$\forall t_q\in \mathrm{substr}(q)$,若存在 $p\to q$ 的转移边,则 $\exist t_p\in \mathrm{substr}(p)$ 使得 $t_p+c_{p,q} = t_q$。

证明:结论 2.1 的逆命题。这很好理解,因为对于任意 $t_q\in \mathrm{substr}(q)$,若不存在这样的 $t_p + c_{p,q} = t_q$,那么就不存在 $T\to q$ 的路径使得其所表示字符串为 $t_p + c_{p,q}$,这与 $t_q\in \mathrm{substr}(q)$ 矛盾。

结论 3.1:考虑状态 $q$,不存在转移 $p\to q$ 使得 $\mathrm{len}(p) + 1 > \mathrm{len}(q)$。

证明:显然。

结论 3.2:考虑状态 $q$,** 唯一 ** 存在状态 $p$ 和转移 $p\to q$ 使得 $\mathrm{len}(p) + 1 = \mathrm{len}(q)$。

证明:考虑反证法,若不存在这样的 $p$,说明 $\forall p,\mathrm{len}(p)+1<\mathrm{len}(q)$。根据结论 2.2,$\mathrm{substr}(q)$ 中最长的一个串的长度为 $\max_{\ t_p\in \mathrm{substr}(p)} |t_p| + 1$ 即 $\max_{\ p} \mathrm{len}(p) + 1$。根据 $\mathrm{len}$ 的定义与 $\mathrm{len}(p) + 1 < \mathrm{len}(q)$,推得 $\mathrm{len}(q) < \mathrm{len}(q)$,矛盾。唯一性不难证明。

简单地说,若数集 $T$ 由若干数集 $S$ 的并加上 $1$ 后得到,那么 $\max_{\ s\in S}s + 1 = \max_{\ t\in T}t$。

结论 3.3:考虑状态 $q$,唯一 存在转移 $p\to q$ 使得 $\mathrm{minlen}(p) + 1 = \mathrm{minlen}(q)$。

证明:同理。

  • 定义 $\mathrm{maxtrans}(q)$ 表示使得 $\mathrm{len}(p) + 1 = \mathrm{len}(q)$ 且存在转移 $p\to q$ 的唯一的 $p$。
  • 定义 $\mathrm{mintrans}(q)$ 表示使得 $\mathrm{minlen}(p) + 1 = \mathrm{minlen}(q)$ 且存在转移 $p\to q$ 的唯一的 $p$。

结论 4.1:考虑状态 $q$,若存在转移 $p\to q$,则 $p$ 在后缀链接树上是 $\mathrm{maxtrans}(q)$ 或其祖先。

证明:由于所有 $p$ 转移到相同状态 $q$,故所有 $p$ 的 $\mathrm{substr}(p)$ 的并,短串为长串的后缀。根据 $\mathrm{link}$ 树的性质即证。

结论 4.2:考虑状态 $q$,若存在转移 $p\to q$,则 $p$ 在后缀链接树上是 $\mathrm{mintrans}(q)$ 或其子节点。

证明:同理。

结论 4.3:考虑状态 $q$,若存在转移 $p\to q$,则所有这样的 $p$ 在 $\mathrm{link}$ 树上形成了一条 深度递减的链 $\mathrm{maxtrans}(q)\to \mathrm{mintrans}(q)$。

证明:结合结论 4.1 与结论 4.2 易证。

可以发现上述性质大都与后缀链接有关,因为后缀链接是 SAM 所提供的最重要的核心信息。我们甚至可以抛弃 SAM 的 DAWG,仅仅使用后缀链接就可以解决大部分字符串相关问题。

  • 扩展定义:$\mathrm{substr}(p\to q)$ 表示后缀路径 $p\to q$ 上所有状态的 $\mathrm{substr}$ 的并。

2.3 构建 SAM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int n;
char s[N];

int lst = 1, node = 1, fa[N], len[N], sz[N];
int ch[N][26];

void ins(int c) {
int p = ++ node, now = lst; lst = p; sz[p] = 1; len[p] = len[now] + 1;
while (now && ! ch[now][c]) ch[now][c] = p, now = fa[now];
if (! now) return fa[p] = 1, void();
int x = ch[now][c];
if (len[x] == len[now] + 1) return fa[p] = x, void();
int y = ++ node; fa[y] = fa[x]; fa[x] = fa[p] = y; len[y] = len[now] + 1;
memcpy(ch[y], ch[x], sizeof(ch[y]));
while (now && ch[now][c] == x) ch[now][c] = y, now = fa[now];
}

当字符集 $\Sigma$ 非常大的时候,时空复杂度均无法接受,因此需要使用平衡树维护每个状态的所有转移边,可以用 map 代替。

2.4 时间复杂度证明

下设字符串 $s$ 长度为 $n$,证明大部分摘自 OI wiki。

2.4.1 状态数上界

构建后缀自动机的算法本身就已经证明了其 SAM 状态数不超过 $2n-1$:插入 $s_1,s_2$ 时分别产生一个状态,后续插入每个 $s_i$ 时最多产生两个状态,因此当 $n>1$ 时状态数不超过 $2n-2$,形如 $\tt abb\cdots bb$ 的字符串达到上界。当 $n=1$ 时状态数为 $2n-1$。

2.4.2 转移数上界

称 $\mathrm{len}(p) + 1 = \mathrm{len}(q)$ 的转移 $(p, q)$ 为连续的,显然,从一个非终止状态 $p$ 出发 有且仅有 一条连续转移 $(p,q)$,对于 $q$ 也有且仅有一个对应的 $p$。因此,连续转移总数不超过 $2n-2$。对于不连续的转移,找到从根节点 $T\to p$ 的一条连续路径,设其所表示字符串为 $u$;找到从 $q$ 到任意一个终止节点 $f\in F$ 的一条连续路径,设其所表示字符串为 $v$。对于不同的 $p,q$,$s_{p,q} = u + c_{p,q} + v$ 互不相同:若两个转移 $(p,q)$ 和 $(p’, q’)$ 出现 $s_{p, q} = s_{p’, q’}$ 的情况,由于不同路径所表示字符串不同,因此 $(p, q)$ 和 $(p’, q’)$ 在同一条路径,这与 $T\to p$ 和 $q\to F$ 连续矛盾。又因为 $s_{p, q}$ 是 $s$ 的真后缀($s$ 对应的路径转移显然连续),因此不连续的转移数量不超过 $n-1$。这样,我们得到了转移数上界 $3n-3$。

由于最大的状态数量仅在形如 $\tt abb \cdots bb$ 的字符串中达到,此时转移数量小于 $3n - 3$。形如 $\tt abb\cdots bbc$ 的字符串达到了 $3n - 4$ 的上界。

2.5 应用

2.5.1 求本质不同子串个数

根据 SAM 的性质,每个子串唯一对应一个状态,因此答案即 $\sum \mathrm{len}(i) - \mathrm{len}(\mathrm{link}(i))$。

2.5.2 字符串匹配

用文本串 $t$ 在 $s$ 的 SAM 上跑匹配时,我们可以得到对于 $t$ 的每个 前缀 $t[1, i]$,其作为 $s$ 的子串出现的 **最长后缀 $L_i$**:若当前状态 $p$(即 $t[i - L_{i - 1}, i - 1]$ 所表示的状态)不能匹配 $t_i$(即 $\delta(p, t_i)$ 不存在),就跳后缀链接令 $p\gets \mathrm{link}(p)$ 并实时更新 $L_i = \mathrm{len}(p)$ 直到 $p = T$ 或 $\delta(p, t_i)$ 存在,对于后者令 $p\gets \delta(p, t_i)$,$L_i$ 还需再加上 $1$。若能匹配,则直接令 $p\gets \delta(p, t_i)$ 并令 $L_i\gets L_{i - 1} + 1$。综合一下,我们得到如下代码:

1
2
3
4
5
6
7
8
for(int i = 1, p = 1, L = 0; i <= n; i++) {

while(p > 1 && !son[p][t[i] - 'a']) L = len[p = fa[p]];

if(son[p][t[i] - 'a']) L = min(L + 1, len[p = son[p][t[i] - 'a']]);

}

2.6 广义 SAM

广义 SAM,GSAM,全称 General Suffix Automaton,相对于普通 SAM 它支持对多个字符串进行处理。它可以看做对 trie 建后缀自动机。

一般的写法是每插入一个字符串前将 $las$ 指针置为 $T$,非常方便。一个细节:构建单串 SAM 时,$\delta(las, s_i)$ 一定不存在,但对于多串 SAM 可能存在。这说明当前字符串 $s$ 的 $i$ 前缀是某个已经添加过的字符串的子串。我们需要进行以下特判,否则会出现这种情况:https://www.luogu.com.cn/discuss/322224

  1. 当 $q = \delta(las, s_i)$ 存在,且 $\mathrm{len}(las) + 1 = \mathrm{len}(q)$ 时,令 $las\gets q$ 并直接返回。
  2. 当 $q = \delta(las, s_i)$ 存在,且 $\mathrm{len}(las) + 1 \neq \mathrm{len}(q)$ 时,我们会新建节点 $cl$,并进行复制。此时,令 $las\gets cl$ 而非 $cur$。这是因为 $\mathrm{len}(cur) = \mathrm{len}(las) + 1$ 且 $\mathrm{len}(cl) = \mathrm{len}(las) + 1$,又因为 $\mathrm{link}(cur) = cl$,所以这说明 $\mathrm{substr}(cur) = \varnothing$,即 节点 $cur$ 是空壳,真正的信息在 $cl$ 上面。为此,我们舍弃掉这个 $cur$,并用 $cl$ 代替它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int ins(int p, int it) {

if(son[p][it] && len[son[p][it]] == len[p] + 1) return son[p][it]; // 如果节点已经存在,且 len 值相对应,即 (p, son[p][it]) 是连续转移,则直接转移。

int cur = ++cnt, chk = son[p][it]; len[cur] = len[p] + 1;

while(!son[p][it]) son[p][it] = cur, p = fa[p];

if(!p) return fa[cur] = 1, cur;

int q = son[p][it];

if(len[p] + 1 == len[q]) return fa[cur] = q, cur;

int cl = ++cnt; cpy(son[cl], son[q], S);

len[cl] = len[p] + 1, fa[cl] = fa[q], fa[q] = fa[cur] = cl;

while(son[p][it] == q) son[p][it] = cl, p = fa[p];

return chk ? cl : cur; // 如果 len[las][it] 存在,则 cur 是空壳,返回 cl 即可

}

上述方法本质相当于对匹配串建出 trie 后进行 dfs 构建 SAM。部分特殊题目会直接给出 trie 而非模板串,此时模板串长度之和的级别为 $\mathcal{O}(|S| ^ 2)$,因此只能 bfs 构建 SAM:设 $P_p$ 表示 trie 树上状态 $p$ 在 SAM 上对应的位置,若 trie 树 $T$ 上的转移 $q = \delta_T(p, c)$ 存在,其中 $c$ 是 $p\to q$ 所表示字符,那么以 $P_p$ 作为 $las$,插入字符 $c$ 后新的 $las$ 即 $P_q$。此时 不需要 像上面一样特判,因为 $\delta(P_p, c)$ 必然不存在,这是由于 bfs 使得 $\mathrm{len}(P_p)$ 单调不降。模板题 P6139 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#include <bits/stdc++.h>

using namespace std;



#define ll long long

#define cpy(x, y, s) memcpy(x, y, sizeof(x[0]) * (s))



const int N = 2e6 + 5;

const int S = 26;



ll n, ans, cnt = 1;

string s;

int len[N], fa[N], son[N][S];

int ins(int p, int it) {

int cur = ++cnt; len[cur] = len[p] + 1;

while(!son[p][it]) son[p][it] = cur, p = fa[p];

if(!p) return fa[cur] = 1, cur;

int q = son[p][it];

if(len[p] + 1 == len[q]) return fa[cur] = q, cur;

int cl = ++cnt; cpy(son[cl], son[q], S);

len[cl] = len[p] + 1, fa[cl] = fa[q], fa[q] = fa[cur] = cl;

while(son[p][it] == q) son[p][it] = cl, p = fa[p];

return cur;

}



int node = 1, pos[N], tr[N][S];

void ins(string s) {

int p = 1;

for(char it : s) {

if(!tr[p][it - 'a']) tr[p][it - 'a'] = ++node;

p = tr[p][it - 'a'];

}

}

void build() {

queue <int> q; q.push(pos[1] = 1);

while(!q.empty()) {

int t = q.front(); q.pop();

for(int i = 0, p; i < S; i++) if(p = tr[t][i])

pos[p] = ins(pos[t], i), q.push(p);

}

}

int main() {

cin >> n;

for(int i = 1; i <= n; i++) cin >> s, ins(s);

build();

for(int i = 2; i <= cnt; i++) ans += len[i] - len[fa[i]];

cout << ans << endl;

return 0;

}

2.7 常用技巧与结论

2.7.1 线段树合并维护 $\mathrm{endpos}$ 集合

对于部分题目,我们需要维护每个状态的 $\mathrm{endpos}$ 集合,以刻画每个子串在字符串中所有出现位置的信息。

为此,我们在 $s[1, i]$ 对应状态的 $\mathrm{endpos}$ 集合里插入位置 $i$,再根据 $\mathrm{endpos}$ 集合构造出来的树本质上就是后缀链接树这一事实,在 $\mathrm{link}$ 树上进行 线段树合并 即可得到每个状态的 $\mathrm{endpos}$ 集合。这是一个非常有用且常见的技巧。

注意,线段树合并时会破坏原有线段树的结构,因此若需要在线段树合并后保留每个状态的 $\rm endpos$ 集合对应的线段树的结构,需要在线段树合并时 新建节点。即 可持久化线段树合并。SAM 相关问题的线段树合并通常均需要可持久化。

特别的,如果仅为了得到 $\mathrm{endpos}$ 集合大小,那么只需求出每个状态在 $\mathrm{link}$ 树上的子树有多少个表示 $s$ 的前缀的状态。前缀状态即所有曾作为 $cur$ 的节点。对此,有两种解决方法:直接建图 dfs,以及 ——

2.7.2 桶排确定 dfs 顺序

显然后缀链接树上父亲的 $\mathrm{len}$ 值一定小于儿子,但千万不能认为编号小的节点 $\mathrm{len}$ 值也小。因此,对所有节点按照 $\mathrm{len}$ 值从大到小进行桶排序,然后按顺序合并每个状态及其父亲是正确的,并且常数比建图 + dfs 小不少,代码见例题 I.

2.7.3 快速定位子串

给定区间 $[l, r]$,求 $s_{l, r}$ 在 SAM 上的对应状态:在构建 SAM 时容易预处理 $s_{1, i}$ 所表示的状态 $pos_i$。从 $pos_r$ 开始在 $\mathrm{link}$ 树上倍增找到最浅的,$\rm len$ 值 $\geq r - l + 1$ 的状态 $p$​ 即为所求。

2.7.4 其它结论

  1. 在 $\rm link$ 树上,若 $p$ 是 $q$ 的祖先,则 $\mathrm{substr}(p)$ 中所有字符串在 $\mathrm{longest}(q)$(下记为 $s$)中出现次数与出现位置相同。具体证明见 CF700E 题解区

2.8 注意点总结

  • 做题时不要忘记初始化 $las$ 和 $cnt$。
  • 第二个 while 不要写成 son[p][it] = cur,应为 son[p][it] = cl
  • SAM 开两倍空间
  • 对于多串 SAM,如果每插入一个新字符串时令 $las\gets T$,且插入字符时不特判 $\delta(las, s_i)$ 是否存在,会导致出现空状态,从而父节点的 $\mathrm{len}$ 值 不一定严格小于 子节点,使得桶排失效。对此要格外注意。

后缀平衡树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;
const double LIM = 1e16;
const int MAXN = 2e6;

int n, m;
char str[MAXN], S[MAXN];
double key[MAXN];
int ch[MAXN][2], siz[MAXN];
int tr[MAXN], rt, tcnt;
char op[10];

void Decode(char *s, int mask) {
int len = strlen(s);
for (int i = 0; i < len; i++) {
mask = (mask * 131 + i) % len;
char t = s[i];
s[i] = s[mask];
s[mask] = t;
}
}

void Update(int now) {
siz[now] = 1 + siz[ch[now][0]] + siz[ch[now][1]];
}

int Bad(int now) {
return 1.0 * siz[ch[now][0]] > 0.7 * siz[now] || 1.0 * siz[ch[now][1]] > 0.7 * siz[now];
}

void DFS(int now) {
if (!now) return;
DFS(ch[now][0]);
tr[++tcnt] = now;
DFS(ch[now][1]);
ch[now][0] = ch[now][1] = 0;
}

void Rebuild(int &now, int l, int r, double lv, double rv) {
if (l > r) return;
int mid = (l + r) >> 1;
double midv = (lv + rv) / 2;
now = tr[mid];
key[now] = midv;
Rebuild(ch[now][0], l, mid - 1, lv, midv);
Rebuild(ch[now][1], mid + 1, r, midv, rv);
Update(now);
}

void Maintain(int &now, double lv, double rv) {
if (Bad(now)) {
tcnt = 0;
DFS(now);
Rebuild(now, 1, tcnt, lv, rv);
}
}

int Comp(int x, int y) {
return S[x] < S[y] || (S[x] == S[y] && key[x - 1] < key[y - 1]);
}

void Insert(int &now, int idx, double lv, double rv) {
if (!now) {
now = idx;
siz[now] = 1;
key[now] = (lv + rv) / 2;
ch[now][0] = ch[now][1] = 0;
return;
}
if (Comp(idx, now)) Insert(ch[now][0], idx, lv, key[now]);
else Insert(ch[now][1], idx, key[now], rv);
Update(now);
Maintain(now, lv, rv);
}

void Remove(int &now, int idx) {
if (now == idx) {
if (!ch[now][0] || !ch[now][1]) {
now = (ch[now][0] | ch[now][1]);
} else {
int cur = ch[now][0], las = now;
while (ch[cur][1]) {
las = cur;
siz[las]--;
cur = ch[cur][1];
}
if (las == now) {
ch[cur][1] = ch[now][1];
now = cur;
Update(now);
} else {
ch[cur][0] = ch[now][0];
ch[cur][1] = ch[now][1];
ch[las][1] = 0;
now = cur;
Update(now);
}
}
return;
}
if (Comp(idx, now)) Remove(ch[now][0], idx);
else Remove(ch[now][1], idx);
Update(now);
}

int Com(int now) {
for (int p = 1; str[p]; p++, now = (now ? now - 1 : 0)) {
if (str[p] < S[now]) return 1;
else if (str[p] > S[now]) return 0;
}
}

int Query(int now) {
if (!now) return 0;
int ls = siz[ch[now][0]];
if (Com(now)) return Query(ch[now][0]);
else return Query(ch[now][1]) + ls + 1;
}

int main() {
scanf("%d", &m);
scanf("%s", S + 1);
int mask = 0, ans = 0;
n = strlen(S + 1);
for (int i = 1; i <= n; i++) Insert(rt, i, 0, LIM);
for (int i = 1; i <= m; i++) {
scanf("%s", op);
if (op[0] == 'A') {
scanf("%s", str + 1);
Decode(str + 1, mask);
int len = strlen(str + 1);
for (int j = 1; j <= len; j++) {
S[n + j] = str[j];
Insert(rt, n + j, 0, LIM);
}
n += len;
} else if (op[0] == 'Q') {
scanf("%s", str + 1);
Decode(str + 1, mask);
int len = strlen(str + 1);
reverse(str + 1, str + len + 1);
str[len + 1] = 'Z' + 1;
str[len + 2] = '\0';
ans = Query(rt);
str[len]--;
ans -= Query(rt);
printf("%d\n", ans);
mask ^= ans;
} else {
int k;
scanf("%d", &k);
for (int j = n; j > n - k; j--) Remove(rt, j);
n -= k;
}
}
return 0;
}

回文

manacher

来自洛谷模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;
using i64 = long long;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for (int i = (l); i <= (r); i ++)
#define rep(i, l, r) for (int i = (l); i >= (r); i --)

const int N = 5e7 + 10;

int n;
char s[N], t[N];
int p[N];

int main() {
std :: ios :: sync_with_stdio(false);
cin >> (s + 1); n = strlen(s + 1);
int len = 0;
t[++ len] = '%';
lep (i, 1, n)
t[++ len] = s[i], t[++ len] = '%';
t[++ len] = '%';
int mx = 0, pos = 0, res = 0;
lep (i, 2, len - 1) {
if (i < mx) p[i] = std :: min(p[pos * 2 - i], mx - i + 1);
else p[i] = 0;
while (t[i + p[i]] == t[i - p[i]]) ++ p[i];
res = std :: max(res, p[i] - 1);
if (i + p[i] - 1 > mx) mx = i + p[i] - 1, pos = i;
}
cout << res << endl;
return 0;
}

PAM

来自洛谷模板, 求每个位置结尾的回文子串个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;
using i64 = long long;

#define endl '\n'
#define debug(...) fprintf(stderr, __VA_ARGS__)
#define lep(i, l, r) for (int i = (l); i <= (r); i ++)
#define rep(i, l, r) for (int i = (l); i >= (r); i --)

const int N = 5e5 + 10;

int n;
char s[N];
int ch[N][26], len[N], node, fa[N], cnt[N], las;

void ins(int id, int c) {
int u = las;
while (s[id - len[u] - 1] != s[id]) u = fa[u];
if (! ch[u][c]) {
int p = ++ node; int v = fa[u]; len[p] = len[u] + 2;
while (s[id - len[v] - 1] != s[id]) v = fa[v];
fa[p] = ch[v][c]; ch[u][c] = p;
cnt[p] = cnt[fa[p]] + 1;
}
las = ch[u][c];
}

int main() {
std :: ios :: sync_with_stdio(false);
cin >> (s + 1); n = strlen(s + 1);
len[1] = -1; node = fa[0] = fa[1] = 1;
for (int i = 1; i <= n; i ++)
ins(i, s[i] - 'a'), s[i + 1] = (s[i + 1] - 97 + cnt[las]) % 26 + 97, cout << cnt[las] << ' ';
return 0;
}

exkmp

给定两个字符串 $a,b$,你要求出两个数组:

  • $b$ 的 $z$ 函数数组 $z$,即 $b$ 与 $b$ 的每一个后缀的 LCP 长度。
  • $b$ 与 $a$ 的每一个后缀的 LCP 长度数组 $p$。

对于一个长度为 $n$ 的数组 $a$,设其权值为 $\operatorname{xor}_{i=1}^n i \times (a_i + 1)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <bits/stdc++.h>

using std :: cin;
using std :: cout;
using std :: cerr;
#define endl '\n'

using i64 = long long;

const int N = 4e7 + 10;

int n, m;
char s[N], _s[N], _t[N];
int z[N];

signed main() {
std :: ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> (_s + 1);
n = strlen(_s + 1);
cin >> (_t + 1);
m = strlen(_t + 1);
std :: swap(n, m);
std :: swap(_s, _t);
for(int i = 1; i <= n; i ++) s[i] = _s[i];
s[n + 1] = '*';
for(int i = 1; i <= m; i ++) s[i + n + 1] = _t[i];
z[1] = n;
int l = 0, r = 0;
for(int i = 2; i <= m + n + 1; i ++) {
if(i <= r) z[i] = std :: min(z[i - l + 1], r - i + 1);
else z[i] = 0;
while(s[z[i] + 1] == s[i + z[i]]) z[i] ++;
if(i + z[i] - 1 > r) r = i + z[i] - 1, l = i;
}
i64 ans = 0;
for(int i = 1; i <= n; i ++) ans ^= 1ll * i * (z[i] + 1);
cout << ans << endl; ans = 0;
for(int i = 1; i <= m; i ++) ans ^= 1ll * i * (z[i + n + 1] + 1);
cout << ans << endl;
return 0;
}

计算几何

记录一些东西。

凸包

采用求一次上凸包然后求一次下凸包在拼起来实现。
$type = 0$ 是下凸包, 反之是上凸包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline void graham(vector<vec2> &vec, int type) {
if(vec.size() == 0) { cerr << "Hull is empty !\n" << endl; exit(0); }
sort(vec.begin(), vec.end());
vector<vec2> rec; rec.pb(* vec.begin()); int sz = 0;
for(int i = 1; i < vec.size(); i ++) {
if(type == 0) while(sz >= 1 && le(cross(rec[sz - 1], rec[sz], vec[i]), 0)) rec.pop_back(), sz --;
else while(sz >= 1 && ge(cross(rec[sz - 1], rec[sz], vec[i]), 0)) rec.pop_back(), sz --;
rec.push_back(vec[i]); sz ++;
}
swap(vec, rec);
}

inline void graham_full(vector<vec2> &vec) {
vector<vec2> v1 = vec, v2 = vec;
graham(v1, 0); graham(v2, 1);
v1.pop_back(); for(int i = v2.size() - 1; i >= 1; i --) v1.push_back(v2[i]); swap(vec, v1);
}

凸包直径

采用旋转卡壳实现。每次就看移动指针以后面积会不会变大就好了。

1
2
3
4
5
6
7
8
9
10
11
inline double convDiameter(vector<vec2> vec) {
if(vec.size() == 2) { return (vec[0] - vec[1]).norm2(); }
vec.pb(vec[0]);
int j = 2, n = vec.size() - 1;
double res = 0;
for(int i = 0; i < vec.size() - 1; i ++) {
while(abs(cross(vec[i], vec[i + 1], vec[j])) < abs(cross(vec[i], vec[i + 1], vec[j + 1]))) j = (j + 1) % n;
Max(res, max((vec[i] - vec[j]).norm2(), (vec[i + 1] - vec[j]).norm2()));
}
return res;
}

直线交点

用三角形面积然后定比分点实现。

1
2
3
4
5
inline vec2 intersection(const line &l1, const line &l2) {
double ls = cross(l1.p1, l1.p2, l2.p1);
double rs = cross(l1.p1, l1.p2, l2.p2);
return l2.p1 + (l2.p2 - l2.p1) * ls / (ls - rs);
}

半平面交

把直线按照极角排序,平行的保留内侧在前, 注意排序判断是否在内侧的时候使用严格大于,从头开始加, 如果队头或者队尾的两条直线交点在当前直线右侧则弹出, 如果最后队尾两条直线交点在队头直线右侧则弹出队尾。
直线是逆时针存的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
inline void HPI(vector<line> &lv) {
vector<pair<line, double> > sorted(lv.size());
for(int i = 0; i < lv.size(); i ++) sorted[i].fi = lv[i], sorted[i].se = atan2(lv[i].direct().y, lv[i].direct().x);
sort(sorted.begin(), sorted.end(), [] (auto a, auto b) -> bool {
if(eq(a.se, b.se)) {
if(lt(cross(a.fi.p1, a.fi.p2, b.fi.p2), 0)) return 1;
else return 0;
}
else return a.se < b.se;
} );
for(int i = 0; i < lv.size(); i ++) lv[i] = sorted[i].fi;
deque<line> q;
q.push_back(lv[0]);
for(int i = 1; i < lv.size(); i ++) if(! parallel(lv[i], lv[i - 1])) {
while(q.size() > 1) {
vec2 p = intersection(* --q.end(), * -- -- q.end());
if(lt(cross(lv[i].p1, lv[i].p2, p), 0)) q.pop_back();
else break;
}
while(q.size() > 1) {
vec2 p = intersection(* q.begin(), * ++ q.begin());
if(lt(cross(lv[i].p1, lv[i].p2, p), 0)) q.pop_front();
else break;
}
q.push_back(lv[i]);
}
while(q.size() > 1) {
vec2 p = intersection(* --q.end(), * -- -- q.end());
if(lt(cross(q.begin() -> p1, q.begin() -> p2, p), 0)) q.pop_back();
else break;
}
lv = vector<line> (q.size());
for(int i = 0; i < q.size(); i ++) lv[i] = q[i];
}

闵可夫斯基和

要求 $v1, v2$ 是凸包且极角排序完毕。
把所有边拿下来归并即可。注意最后弹出一个初始点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
inline vector<vec2> Minkowski(vector<vec2> v1, vector<vec2> v2) {
// v1, v2 is sorted
vector<vec2> s1(v1.size()), s2(v2.size());
for(int i = 1; i < s1.size(); i ++) s1[i - 1] = v1[i] - v1[i - 1]; s1[s1.size() - 1] = v1[0] - v1[s1.size() - 1];
for(int i = 1; i < s2.size(); i ++) s2[i - 1] = v2[i] - v2[i - 1]; s2[s2.size() - 1] = v2[0] - v2[s2.size() - 1];
vector<vec2> hull(v1.size() + v2.size() + 1);
int p1 = 0, p2 = 0, cnt = 0;
hull[cnt ++] = v1[0] + v2[0];
while(p1 < s1.size() && p2 < s2.size()) {
hull[cnt] = hull[cnt - 1] + (ge(s1[p1] ^ s2[p2], 0) ? s1[p1 ++] : s2[p2 ++]);
cnt ++;
}
while(p1 < s1.size()) hull[cnt] = hull[cnt - 1] + s1[p1 ++], cnt ++;
while(p2 < s2.size()) hull[cnt] = hull[cnt - 1] + s2[p2 ++], cnt ++;
hull.pop_back();
return hull;
}

辛普森积分

积分公式 : $(R - L) \times (f(L) + f(R) + 4f(M)) \times \frac{1}{6}$

1
2
3
4
5
6
7
8
9
10
11
db calc(db L, db R) {
db mid = (L + R) / 2;
return (f(L) + f(R) + 4 * f(mid)) / 6 * (R - L);
}

db simpson(db L, db R, int dep) {
if(abs(L - R) <= 1e-9 && dep >= 4) return 0;
db mid = (L + R) / 2;
if(abs(calc(L, R) - calc(L, mid) - calc(mid, R)) <= 1e-9 && dep >= 4) return calc(L, R);
return simpson(L, mid, dep + 1) + simpson(mid, R, dep + 1);
}

圆圆交点

把一个圆放到圆心, 另外一个旋转到水平, 解方程以后转回去。

平面图转对偶图

使用最小左转法。把所有边拆为两条有向边, 然后在每个点上极角排序, 每次一个边在终点找反向边然后看极角序在反向边前面的一条边记为 $nxt$ , 然后跳 $nxt$ 编号, 根据面积正负判断是否是无界域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void build() { 
for(int i = 1; i <= n; i ++) sort(e[i].begin(), e[i].end());
for(int i = 2; i <= tot; i ++) {
int y = E[i].y;
auto it = lower_bound(e[y].begin(), e[y].end(), E[i ^ 1]);
if(it == e[y].begin()) it = -- e[y].end(); else -- it;
nxt[i] = it -> id;
}
for(int i = 2; i <= tot; i ++) if(! pos[i]) {
pos[i] = pos[nxt[i]] = ++ cnt;
int u = E[i].x;
for(int j = nxt[i]; E[j].y != E[i].x; j = nxt[j], pos[j] = cnt)
s[cnt] += ( (nod[E[j].x] - nod[u]) ^ (nod[E[j].y] - nod[u]) );
if(s[cnt] < 1e-9) rt = cnt;
}
for(int i = 2; i <= tot; i ++) {
hv[pos[i]].pb(i);
g[pos[i]].pb( {E[i].w, pos[i ^ 1]} ) ;
}
}

常用操作全家桶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
struct circle{
point C;
double r;
circle(){
}
circle(point C, double r): C(C), r(r){
}
};
pair<point, point> line_circle_intersection(line L, circle C){
point P = projection(C.C, L);
double d = point_line_distance(C.C, L);
double h = sqrt(C.r * C.r - d * d);
point A = P + vec(L) / abs(vec(L)) * h;
point B = P - vec(L) / abs(vec(L)) * h;
return make_pair(A, B);
}
pair<point, point> circle_intersection(circle C1, circle C2){
double d = dist(C1.C, C2.C);
double m = (C1.r * C1.r - C2.r * C2.r + d * d) / (d * 2);
point M = C1.C + (C2.C - C1.C) / d * m;
double h = sqrt(C1.r * C1.r - m * m);
point H = rotate90(C2.C - C1.C) / d * h;
return make_pair(M - H, M + H);
}
pair<point, point> circle_tangent(point P, circle C){
double d = dist(P, C.C);
double r = sqrt(d * d - C.r * C.r);
return circle_intersection(C, circle(P, r));
}
vector<line> common_tangent(circle C1, circle C2){
if (C1.r < C2.r){
swap(C1, C2);
}
double d = dist(C1.C, C2.C);
vector<line> L;
if (C1.r - C2.r <= d + eps){
if (C1.r - C2.r <= eps){
point D = rotate90(C2.C - C1.C) / d * C1.r;
L.push_back(line(C1.C + D, C2.C + D));
L.push_back(line(C1.C - D, C2.C - D));
} else {
double m = (C1.r - C2.r) * (C1.r - C2.r) / d;
point M = C1.C + (C2.C - C1.C) / d * m;
double h = sqrt((C1.r - C2.r) * (C1.r - C2.r) - m * m);
point H1 = M + rotate90(C2.C - C1.C) / d * h;
point D1 = (H1 - C1.C) / dist(H1, C1.C) * C2.r;
L.push_back(line(H1 + D1, C2.C + D1));
point H2 = M - rotate90(C2.C - C1.C) / d * h;
point D2 = (H2 - C1.C) / dist(H2, C1.C) * C2.r;
L.push_back(line(H2 + D2, C2.C + D2));
}
}
if (C1.r + C2.r <= d + eps){
double m = (C1.r + C2.r) * (C1.r + C2.r) / d;
point M = C1.C + (C2.C - C1.C) / d * m;
double h = sqrt((C1.r + C2.r) * (C1.r + C2.r) - m * m);
point H1 = M + rotate90(C2.C - C1.C) / d * h;
point D1 = (H1 - C1.C) / dist(H1, C1.C) * C2.r;
L.push_back(line(H1 - D1, C2.C - D1));
point H2 = M - rotate90(C2.C - C1.C) / d * h;
point D2 = (H2 - C1.C) / dist(H2, C1.C) * C2.r;
L.push_back(line(H2 - D2, C2.C - D2));
}
return L;
}
struct point{
double x, y;
point(){
}
point(double x, double y): x(x), y(y){
}
point operator +(point P){
return point(x + P.x, y + P.y);
}
point operator -(point P){
return point(x - P.x, y - P.y);
}
point operator *(double k){
return point(x * k, y * k);
}
point operator /(double k){
return point(x / k, y / k);
}
};
point rotate90(point P){
return point(-P.y, P.x);
}
point rotate(point P, double t){
return point(P.x * cos(t) - P.y * sin(t), P.x * sin(t) + P.y * cos(t));
}
double abs(point P){
return sqrt(P.x * P.x + P.y * P.y);
}
double dist(point P, point Q){
return abs(Q - P);
}
double dot(point P, point Q){
return P.x * Q.x + P.y * Q.y;
}
double cross(point P, point Q){
return P.x * Q.y - P.y * Q.x;
}
struct line{
point A, B;
line(){
}
line(point A, point B): A(A), B(B){
}
};
point vec(line L){
return L.B - L.A;
}
bool point_on_segment(point P, line L){
return dot(P - L.A, vec(L)) > -eps && dot(P - L.B, vec(L)) < eps;
}
point projection(point P, line L){
return L.A + vec(L) / abs(vec(L)) * dot(P - L.A, vec(L)) / abs(vec(L));
}
point reflection(point P, line L){
return projection(P, L) * 2 - P;
}
double point_line_distance(point P, line L){
return abs(cross(P - L.A, vec(L))) / abs(vec(L));
}
double point_segment_distance(point P, line L){
if (dot(P - L.A, vec(L)) < 0){
return dist(P, L.A);
} else if (dot(P - L.B, vec(L)) > 0){
return dist(P, L.B);
} else {
return point_line_distance(P, L);
}
}
bool is_parallel(line L1, line L2){
return abs(cross(vec(L1), vec(L2))) < eps;
}
point line_intersection(line L1, line L2){
return L1.A + vec(L1) * cross(L2.A - L1.A, vec(L2)) / cross(vec(L1), vec(L2));
}
bool segment_intersect(line L1, line L2){
return cross(L1.A - L2.A, vec(L2)) * cross(L1.B - L2.A, vec(L2)) < eps && cross(L2.A - L1.A, vec(L1)) * cross(L2.B - L1.A, vec(L1)) < eps;
}
double segment_distance(line L1, line L2){
if (segment_intersect(L1, L2)){
return 0;
} else {
double ans = INF;
ans = min(ans, point_segment_distance(L1.A, L2));
ans = min(ans, point_segment_distance(L1.B, L2));
ans = min(ans, point_segment_distance(L2.A, L1));
ans = min(ans, point_segment_distance(L2.B, L1));
return ans;
}
}

杂技

线性基

构造

1
2
3
4
5
6
7
inline void ins(LL x) {
for(R int i=62;i>=0;i--)
if(x&(1LL<<i)) {
if(!p[i]) { p[i]=x,cnt++; break; }
else x^=p[i];
}
}

查询一个元素是否可以被异或出来

从高到低,如果这一位为$1$就异或上这一位的线性基,把$1$消去,根据性质一,如果最后得到了$0$,那这个数就可以表示出来。

1
2
3
4
5
inline int ask(LL x) {
for(R int i=62;i>=0;i--)
if(x&(1LL<<i)) x^=p[i];
return x==0;
}

查询异或最大值

按位贪心即可。

1
2
3
4
5
6
inline LL askmx() {
LL ans=0;
for(R int i=62;i>=0;i--)
if((ans^p[i])>ans) ans^=p[i];
return ans;
}

查询异或最小值

其实异或的最小值一般来说就是线性基里的最小元素,因为插入这个元素的时候我们总是尽量让它的高位为$0$才来插入这一位。但是为什么是“一般”呢?因为有可能会有出现$0$,得要在插入的时候记下个标记来特判才行。

1
2
3
4
5
inline LL askmn() {
if(zero) return 0;
for(R int i=0;i<=62;i++)
if(p[i]) return p[i];
}

查询异或第$k$小

1
2
3
4
5
6
7
inline void rebuild() {
cnt=0;top=0;
for(R int i=MB;i>=0;i--)
for(R int j=i-1;j>=0;j--)
if(p[i]&(1LL<<j)) p[i]^=p[j];
for(R int i=0;i<=MB;i++) if(p[i]) d[cnt++]=p[i];
}
1
2
3
4
5
6
7
inline LL kth(int k) {
if(k>=(1LL<<cnt)) return -1;
LL ans=0;
for(R int i=MB;i>=0;i--)
if(k&(1LL<<i)) ans^=d[i];
return ans;
}

但是这样其实还不太对,因为我们并没有考虑$0$的情况,所以还要去考虑一下$0$的情况,特判即可。

1
printf("%lld\n",tmp-zero?kth(tmp-zero):0LL);

查询排名

1
2
3
4
5
6
inline int rank(LL x) {
int ans = 0;
for(R int i = cnt - 1; i >= 0; i --)
if(x >= d[i]) ans += (1 << i), x ^= d[i];
return ans + zero;
}

注:这个$d[i]$是重建后的线性基。

部分结论 & 定理 & 技巧

Prufer 序列

树转 Prufer

不断将编号最小的叶子节点删除并将其父节点加入 Prufer 序列

可以证明树中至少有 $2$ 个叶子,所以 $n$ 号点必然留到最后,也就是可以把 $n$ 当作根先求出每个点的父节点

考虑用一个指针维护最小叶子,每删一个叶子,至多只能多一个叶子,所以直接判其是否小于当前指针

如果是,将其加入 Prufer ;否则,继续枚举指针,因为不影响

不难发现每一种不区分儿子顺序的无根树都能唯一对应一种 Prufer 序列

这里也可以看出, Prufer 中一个点出现的次数等于其点度减 $1$

Prufer 转树

类似根据树转 Prufer 的做法考虑通过 Prufer 倒推树

从前往后枚举 Prufer 序列,每次取出在之后没出现,且还没选过的点确认为当前选出的叶子,根据 Prufer 就可以知道它的父亲

从小到大枚举数,同时维护一个在 Prufer 上从前往后的指针

找到一个合法的叶子时,它的父亲就是指针指向的 Prufer 的值,同时向后移动指针

此时它的父亲可能成为新的叶子,当它的父亲编号小于当前枚举的数时继续确定树上结构即可

注意 Prufer 的长度为 $n-2$ 所以要在后面加一个 $n$ 以指示最后一个被删的数的父亲同时防止操作越界

同时也不难发现每一个 Prufer 序列唯一对应一棵树,因此无根树是与 Prufer 序列一一对应的

一些推论或应用

  • 给定点度数计数

直接可以得出每个点在 Prufer 中的出现次数 $d_i$,答案就是 $\frac{(n-2)!}{\prod_{i} d_i!}$


  • 无根树森林计数

考虑建立无根树的 EGF


  • 有根树森林计数

考虑把每棵树的跟连到 $n+1$ 上,直接做无根树计数,然后把与 $n+1$ 相连的边断开即可


  • 有根树森林给定根计数

设给定了 $P$ 个根,此时只要这个序列的最后一位是某一个根并且序列长度为 $n-P$ 就一定能对应一种合法森林

根据一般 Prufer 的处理方式,我们不选择维护最后一位为根,而是去掉最后一位,强制某个根少出现一次

此时无法维护每个点各为一棵树的情况,具体做题的时候可能需要特判

拓展:图联通计数

$n$ 个点 $m$ 条边的图,若其有 $K$ 个联通块,求添加 $K-1$ 条边使其联通的方案数

设联通块 $i$ 的点度为 $d_i$ ,则总方案为
$$
\binom{K-2}{d_1-1,d_2-1,\dots,d_K-1}=\frac{(K-2)!}{(d_1-1)!(d_2-1)!\dots(d_K-1)!}
$$
设联通块 $i$ 的点数为 $s_i$ ,则其连边的方案为 $s_i^{d_i}$ ,则枚举 $d$ 序列有总方案数为
$$
\sum_{d_i\geq 1 \ and\ \sum d_i=2K-2}\binom{K-2}{d_1-1,d_2-1,\dots,d_K-1}*\prod_{i=1}^Ks_i^{d_i}
$$
考虑多项式定理
$$
(x_1+\dots+x_m)^p=\sum_{c_i\geq0\ and\ \sum c_i=p}\binom{p}{c_1,c_2,\dotsm c_m}*\prod_{i=1}^mx_i^{c_i}
$$
换元 $e_i=d_i-1\ and\ \sum e_i=K-2$ 得
$$
\begin{align}
&\sum_{e_i\geq0\ and\ \sum e_i=K-2}\binom{K-2}{e_1,e_2,\dots,e_K}*\prod_{i=1}^Ks_i^{e_i+1}\
&=(s_1+s_2+\dots+s_K)^{K-2}*\prod_{i=1}^K s_i \
&=n^{K-2}\prod_{i=1}^Ks_i
\end{align}
$$
得到了一个很简洁的答案

无向图环计数

三元环计数

对无向图定向,每条边由度数大的点指向度数小的点,若度数相同则由标号大的指向标号小的

此时图变成了 $DAG$,那么先枚举一个点 $u$,将其在 DAG 上可达的点打上标记

再枚举它在 DAG 上指向的一个点 $v$,然后再枚举 $v$ 的可达点,若被标记了,则这个点和 $u,v$ 共同构成三元环

此时对于一条边 $u\rightarrow v$,它被遍历的次数为 $in_u$ 即 $u$ 在 DAG 中的入度

显然 $deg_u\geq in_u$,因此 $u$ 及其前面的点共需要使用 $O(in_u^2)$ 条边,因此 $in_u$ 是 $O(\sqrt m)$ 级别的

故总复杂度为 $O(m\sqrt m)$ 的

四元环计数

首先类似三元环一样将图定向形成 DAG

那么对于一个四元环,它的结构应该是 $u\rightarrow v\rightarrow v’\rightarrow w,u\rightarrow w$ 或者 $u\rightarrow v,v’\rightarrow w$

其中 $u,w$ 是拓扑序中最小和最大的点

因此我们的做法为对于点 $u$,枚举它的可达点 $v$,同时枚举 $v$ 在无向图上的可达点 $w$ 且满足在拓扑序上 $u<w$,给答案加上 $w$ 的标记后让 $w$ 的标记增大 $1$

此时复杂度也是 $O(m\sqrt m)$ 的

竞赛图

有向完全图的一种叫法

哈密顿路径

这里给出一种提取竞赛图哈密顿路径的做法,同时也可以视为竞赛图一定存在哈密顿路径的一个证明

考虑分治解决问题,将点集分成两部分然后分别求解

考虑合并两条哈密顿路径

类似于归并排序,从两条路径的起点开始枚举

对于现在在枚举的节点,考虑它们中间的边,将这条边的起点加入合并的路径,并枚举下一个节点

类似图中合并 $[1,4]$ 与 $[5,6]$,因为存在边 $1\Rightarrow 5$,所以将 $1$ 压入合并路径,并继续合并 $[2,4]$ 与 $[5,6]$,正确性显然

哈密顿回路

强联通的竞赛图一定包含一条哈密顿回路

考虑在哈密顿路径上做

一定可以找到一个最靠后的点使得它有一条指向开头的边,那么这个子图就有哈密顿回路

然后往后拓展,如果一个点没有一条指向哈密顿回路的边,那么就往后走

因为强连通,所以一定有边指向哈密顿回路

若此时已积累一条单向链,那么直接选该边,被指向点在哈密顿回路上的前驱一定指向单向链起点

否则此时一定可以找到哈密顿回路上相邻两个点,使得后一个被当前点指向,前一个点指向当前点

另外一个与强联通相关的性质是竞赛图缩点后一定为一条链

兰道定理

将一个竞赛图的出度序列从小到大排列

它是一个合法的序列当且仅当
$$
\forall 1\leq k \leq n,\sum_{i=1}^k a_i\geq \binom k2
$$
当然,显然 $k=n$ 时右边的式子要取等

必要性是这 $k$ 个点内部必须是连满的,还有可能往外连边

充分性略

Erdős–Gallai 定理

虽然与竞赛图没什么关系,但是还是放在这里

将无向图的度数从大到小排列

它是一个合法序列当且仅当,度数之和为偶数,且
$$
\forall 1\leq k \leq n,\sum_{i=1}^k d_i-\sum_{i=k+1}^n\min(d_i,k) \leq k(k-1)
$$
必要性是假设这 $k$ 个点尽可能连到其它点上后剩下的度数足够内部消化

欧拉公式

定义域数 $d$ 为平面图将平面划分出的数目

那么一张联通平面图有 $d=m-n+2$

而有 $k$ 个联通块的平面图有 $d=m-n+k+1$

进而拓展得到对于一张不一定联通的平面图有 $d+n-m\geq 2$

  • 极大平面图

无限分割域可以使得每个域都只有三条边 $3d\leq2m$ ,且平面图联通

带入欧拉公式 $\begin{cases}m\leq 3n-6\ d\leq2n-4\end{cases}$

  • 最小度点

设点度最小为 $k$,显然在极大平面图中才会让点度更大,联立方程 $\begin{cases}kn\leq 2m \ 3d= 2m\end{cases}$ 解得 $kn\leq 6n-12$

最终得到 $k \leq 5$

非平面图的判定

一张图是非平面图当且仅当缩掉所有度数为 $2$ 的点后存在 $K_5$ 或 $K_{3,3}$ 子图

数论

费马小定理

对于质数 $p$,若 $gcd(a,p)=1$,则 $a^{p-1}\equiv1 \pmod p$

欧拉定理

若 $a\bot m$,则 $a^{\varphi(m)} \equiv 1 \pmod m$


拓展欧拉定理

$$
a^c \equiv
\begin{cases}
a^{c \bmod \varphi(m)} &\gcd(a,m)=1 \
a^c &\gcd(a,m) \neq 1,c<\varphi(m) \
a^{\left(c \bmod \varphi(m)\right)+\varphi(m)} &\gcd(a,m) \neq 1,c \geq \varphi(m)
\end{cases}
\pmod m
$$

O(1) 快速乘

1
2
3
4
5
6
ll ksc(ll x,ll y,ll mod)
{
ll d=(long double)x/mod*y+0.5;
ll bck=x*y-d*mod;
return bck<0?bck+mod:bck;
}

听说因为一些奇怪的溢出又溢回来的奇怪问题,总之它就是对的

因为 long double 的精度问题所以小概率会锅,但架不住它快啊

补自 2021.11.17,似乎 __int128 的存在使得快速乘没有价值了

补自 2021.12.15,CTT Day4 证明了 __int128long double 常数大


Exgcd

对于不定方程 $ax+by= gcd(a,b)$

$\because gcd(a,b)=gcd(b,a\bmod b)$

设 $bx_0+(a\bmod b)y_0= gcd(b,a\bmod b)$

则 $ax+by=bx_0+(a-\lfloor\frac ab\rfloor*b)y_0$

待定系数可得 $\begin{cases}x=y_0 \ y=x_0-\lfloor\frac ab\rfloor*y_0 \end{cases}$

注意它求出来的 $x,y$ 可能是负数

另外,这种求法求出的是 $|x|+|y|$ 最小的一组解

中国剩余定理

当合并同余方程组时
$$
\begin{cases}
a_1\equiv b_1 \pmod {p_1} \
a_2\equiv b_2 \pmod {p_2} \
\ldots\ldots\
a_n\equiv b_n \pmod {p_n}
\end{cases}
$$
若 $\forall i\neq j,p_i\bot p_j$ 则有一种简单的合并方式

显然最后的模数是 $M=\prod_{i=1}^n p_i$

设 $M_i=\frac M {p_i}$ ,那么显然 $M_i \bot p_i$

设 $t_i\equiv \frac 1 {M_i} \pmod {p_i}$,这个可以通过 Exgcd 求

那么我们可以构造一个显然正确的答案 $\sum_{i=1}^n b_iM_it_i$


Excrt

直接合并同余方程就行了,注意除掉 gcd 再做之类问题

可用性广一些,但细节也多一些

Lucas 定理

$$
\binom{n}{m}\equiv \binom{n/P}{m/P}*\binom{n\bmod P}{m\bmod P} \pmod P
$$

其中 $P$ 是质数

这个式子的意义在于优化组合数求解的速度

另外,组合数求解只在 $n,m<P$ 的时候才能逆元求解,否则无法处理出现上下约分的情况,而 Lucas 定理可以将 $n,m$ 减小到 $p$ 以下


  • ExLucas

考虑分解 $mod=\prod p_i^{b_i}$,显然可以对每个 $p_i^{b_i}$ 分别求解然后 Excrt 合并

现在问题转化为了求解 $\binom{n}{m}\equiv\frac{n!}{m!(n-m)!} \pmod {p^k}$

考虑分开算每个阶乘,比如 $n!=p^e*q\ (q\bot p)$

那么我们可以将 $p^e$ 提出来算,而 $q\bot p$,所以是有逆元存在的

考虑 $n!$ 相当于按 $p^k$ 的长度分段,把 $p$ 的倍数拿出来除掉 $p$ 继续递归,其余互质的部分在模 $p^k$ 意义下显然可以直接预处理

库默尔定理

令 $v_p(n)$ 表示 $n$ 中质因子 $p$ 的次数,则 $v_p(\binom{n}{m})$ 为 $m$ 与 $n-m$ 在 $p$ 进制下的加法进位次数

这里考虑 $\binom nm=\frac {n!}{m!(n-m)!}$

而 $x!$ 中 $p$ 的次数为 $\sum_{i=0}^\infty \lfloor\frac x {p^i}\rfloor$

那么 $m$ 与 $n-m$ 在 $p$ 进制下每次进位就会在某个 $i$ 处多贡献 $1$

威尔逊定理

$$
p\in \text{prime} \iff (p-2)!\equiv 1 \pmod p
$$

当 $p$ 为合数时显然 $(p-2)!\equiv 0 \pmod p$,因此满足必要性

当 $p$ 为质数时,这些数都有逆元,一一搭配得到 $1$,因此也满足充分性

乘法逆元

  1. Exgcd 法:考虑欲求 $x^{-1}*x \equiv 1 \pmod p$,转化为 $x^{-1}x+py=1$,解线性不定方程即可
  2. 费马小定理法:根据费马小定理 $x^{p-1} \equiv 1 \pmod p$ 得 $x^{-1}\equiv x^{p-2} \pmod p$
  3. 线性递推法:考虑带余除法 $mod=x*t \cdots r$,有 $r\equiv -xt \pmod p \iff x^{-1}\equiv - tr^{-1} \pmod p$

原根与乘法群

定义 $g$ 为 $\Z^*_n$(整数模 $n$ 乘法群,其中元素是模 $n$ 的互质同余类)的原根,那么 $g^{0}\sim g^{\varphi(n)-1}$ 为 $1\sim \varphi(n)$


原根基础

一个整数 $n$ 有原根 $\iff n=2、4、p^k、2p^k$

若 $n$ 有原根,则其原根个数为 $\varphi(\varphi(n))$,有个感性的理解方式把这个剩余系绕成一个环,一个 $g$ 就对应一个可以遍历整个环的步长,那这个步长肯定与环长互质

对于原根的判定,我们需要使得 $\forall x,0<x<\varphi(n),g^x\not \equiv1$

考虑若 $g^x\equiv1$,那么 $g^{xy}\equiv 1$,所以我们只需判断 $\forall p\mid\varphi(n),g^{\frac{\varphi(n)}{p}}\not\equiv1$ 即可


离散对数

对于 $\Z^*_p$ 中每个数 $x$,称以 $g$ 为原根时 $y$ 是它的离散对数 $\iff g^y\equiv x\pmod p$

离散对数可以将 $\Z^*p$ 中的乘法转化为 $\Z^*{\varphi(p)}$ 中的加法

求解方法可以使用后面提到的 BSGS

这里再给出一个 Pohlig-Hellman 算法,主要用于处理 $\varphi(p)$ 的质因子较小的情况

我们知道 $g$ 的阶是 $\varphi(p)$,从中分解出一个质数 $\varphi(p)=p’q$,那么我们可以假设 $g^{p’u+v}\equiv x \pmod p$

考虑两边同时取 $q$ 次幂,因为 $g^{p’q}\equiv1 \pmod p$,所以 $g^{vq}\equiv (g^{q})^v\equiv x^{q} \pmod p$,因为 $g^q$ 的阶是 $p’$,那么就可以在 $O(\sqrt{p’})$ 的复杂度中求出 $v$

将逆元带入原式 $g^{p’u}\equiv (g^{p’})^u\equiv x*g^{-v} \pmod p$,那么阶就变成了 $\frac{\varphi(p)}{p’}$ 继续递归求解 $u$


定义 $ord_n(x)=k$,表示最小的正整数 $k$ 使得 $x^k \equiv1 \pmod n$,也可以理解为是 $x$ 的幂在模 $n$ 意义下的环长

显然有 $ord_n(x)\mid \varphi(n)$

不难发现原根其实就是 $ord_n(g)=\varphi(n)$

另外在存在原根的时候不难发现 $gcd(\ln_n(x),\varphi(n))*ord_n(x)=\varphi(n)$,也就是步长乘以步数等于环长

求阶的思路比较简单,直接分解 $\varphi(n)$,初始将 $ord_n(x)$ 置为 $\varphi(n)$,然后不断试除质因子判断是否还有 $x^{ord_n(x)}\equiv1 \pmod n$即可

二次剩余

对于奇质数 $p$ 和整数 $x\bot p$,若存在 $a$ 使得 $a^2\equiv x \pmod p$,我们称 $x$ 在模 $p$ 意义下是一个二次剩余

实际实现注意特判 $x=0$


  • 勒让德符号

定义勒让德符号 $(\frac xp)=x^{\frac{p-1}2}$

考虑 $x^{p-1}\equiv 1 \pmod p$ ,那么有 $(x^{\frac {p-1}2}+1)(x^{\frac{p-1}2}-1)\equiv 0 \pmod p$

因此 $x^{\frac {p-1}2}$ 必定为 $1$ 或 $-1$

若其为 $-1$ 则一定无解,否则 $\sqrt x ^{p-1}\not\equiv 1 \pmod p$,矛盾

接下来因为 $p$ 为奇质数,所以存在原根,设其为 $g$

有 $g^{y\frac{p-1}2}\equiv 1$,又因为 $g$ 的阶为 $p-1$,所以 $p-1\mid y\frac{p-1}2$,因此 $2\mid y$,那么 $g^{\frac y2}$ 即为 $x$ 的二次剩余
$$
(\frac{x}{p})=
\begin{cases}
-1,\quad \not\exist a,a^2\equiv x \pmod p\
1,\quad \exist a,a^2 \equiv x \pmod p\
0,\quad p\mid x
\end{cases}
$$


  • 高斯二次互反律

对于奇质数 $p,q$ 有 $(\frac pq)(\frac qp)=(-1)^{\frac{p-1}2\frac{q-1}2}$

且对于奇质数 $c$ 有 $(\frac{ab}c)=(\frac ac)*(\frac bc)$ ,这个显然


  • Cipolla

考虑随机一个数 $n$ 使得 $(\frac{n^2-x}{p})=-1$,可以证明大约有一半 $n$ 满足条件,所以期望只需要随机 $2$ 次即可

考虑令 $\omega\equiv\sqrt{n^2-x} \pmod p$,具体运算类比复数

我们有一个性质 $\omega^p\equiv (\omega^2)^{\frac{p-1}2}*\omega\equiv -\omega \pmod p$

还有一个性质 $(a+b)^p=\sum_{i=0}^p \binom{p}{i}a^ib^{p-i}$,显然只有 $i=0/p$ 时 $\binom{p}{i}\not\equiv0 \pmod p$,因为 $p$ 作为奇质数一定不会被小于它的数约分掉,因此 $(a+b)^p\equiv a^p+b^p \pmod p$

那么 $a=(n+\omega)^{\frac{p+1}2}$ 就是 $a^2\equiv x \pmod p$ 的一根
$$
\begin{aligned}
a^2
&\equiv (n+\omega)^{p+1}\
&\equiv (n+\omega)^p*(n+\omega)\
&\equiv (n^p+\omega^p)*(n+\omega)\
&\equiv (n-\omega)(n+\omega)\
&\equiv n^2-(n^2-x)\
&\equiv x \pmod p
\end{aligned}
$$
那么我们只需要维护一个类似于虚数的加、乘法运算即可

另外,考虑一个二次方程最多只有两个根,而对于 $a^2\equiv (p-a)^2 \equiv x \pmod p$ 确实有两个解,不会存在增根,所以解出来的虚数中 $\omega$ 前的系数必然为 $0$

这里可能需要特判 $a=0$

Baby Step Giant Step(BSGS)

对于形如求解 $a^k=b$ 在 $[0,n]$ 中的最小非负整数解 $k$ 的问题,其中的乘法是一个满足结合律的半群(走若干次以后进入循环,一开始的一截可能不在循环内)

考虑取 $B=\sqrt n$,预处理出 $a^0,a^1,\ldots,a^{B-1}$ 和 $a^B,a^{2B},\ldots,a^{\lfloor\frac{n}{B}\rfloor B}$

将第二个集合压入哈希表,在第一个集合中枚举,若存在 $p,q$ 使得 $b*a^p=a^{qB}$,则 $b$ 有可能等于 $a^{qB-p}$

然后比如我们的运算存在逆元的时候 $b$ 一定等于 $a^{qB-p}$,否则需要带入验算

类欧几里得算法

  • 前置基础知识

$$
x>\frac ab \iff x>\lfloor\frac ab\rfloor \
x\leq \frac ab \iff x\leq \lfloor\frac ab\rfloor \
$$


  • 原型

$$
f(a,b,c,n)=\sum_{i=0}^n \lfloor\frac{ai+b}c\rfloor
$$

首先特判掉 $a=0$ 和 $n<0$ 的情况

然后我们可以用比较显然的办法简化一下问题
$$
\begin{aligned}
f(a,b,c,n)
&=\sum_{i=0}^n \lfloor\frac{ai+b}c\rfloor \
&=\sum_{i=0}^n \lfloor\frac{(\lfloor\frac ac\rfloor c+a\bmod c)i+(\lfloor\frac bc\rfloor c+b\bmod c)}{c}\rfloor \
&=\lfloor\frac ac\rfloor\frac{n(n+1)}2+\lfloor\frac bc\rfloor (n+1)+\sum_{i=0}^n \lfloor\frac{(a\bmod c)i+(b\bmod c)}{c}\rfloor \
&=\lfloor\frac ac\rfloor
\frac{n(n+1)}2+\lfloor\frac bc\rfloor (n+1)+f(a\bmod c,b\bmod c,c,n)
\end{aligned}
$$
这样就只需要考虑 $a<c,b<c$ 的情况了

考虑对条件与贡献放缩与转化,具体而言原式中 $0\leq i \leq n$ 是条件,而 $\lfloor\frac{ai+b}c \rfloor$ 是贡献

一般而言想要优化一个式子通常采用的是贡献合并,然而这里的贡献因为取整符号而难以合并,那么我们不妨转化条件与贡献
$$
\begin{aligned}
f(a,b,c,n)
&=\sum_{i=0}^n\sum_{j=0}^{\lfloor\frac{ai+b}c \rfloor-1}1 \
&=\sum_{j=0}^{\lfloor\frac{an+b}c \rfloor-1}\sum_{i=0}^n [j<\lfloor\frac{ai+b}c \rfloor]
\end{aligned}
$$
改一下后面的式子
$$
j<\lfloor\frac{ai+b}c \rfloor \iff j+1\leq\lfloor\frac{ai+b}c \rfloor \iff j+1\leq \frac{ai+b}c
$$
继续变换
$$
j+1\leq\frac{ai+b}c \iff jc+c-b-1<ai\iff\lfloor\frac{jc+c-b-1}a\rfloor<i
$$
令 $m=\lfloor\frac{an+b}c\rfloor$,此时原式变为
$$
\begin{aligned}
f(a,b,c,n)
&=\sum_{j=0}^{m-1}\sum_{i=0}^n[\lfloor\frac{jc+c-b-1}a\rfloor<i] \
&=\sum_{j=0}^{m-1}(n-\lfloor\frac{jc+c-b-1}a\rfloor) \
&=nm-f(c,c-b-1,a,m-1)
\end{aligned}
$$
此时 $a,c$ 调换了顺序,然后继续辗转相除,就可以递归处理,不难发现复杂度是 $O(\log c)$ 的


  • 拓展式

你已经学会四则运算了,快来完成这道微积分例题吧!

$$
g(a,b,c,n)=\sum_{i=0}^n i\lfloor\frac{ai+b}c\rfloor
$$

首先还是除掉 $a,b$ 中多出的部分,令 $A=\lfloor\frac ac\rfloor,B=\lfloor\frac bc\rfloor$,求解 $\sum_{i=0}^n i(Ai+B)$,直接套用公式即可

那么接下来默认 $a<c,b<c$

令 $m=\lfloor\frac{an+b}c\rfloor,t=\lfloor\frac{cj+c-b-1}a\rfloor,N=(c,c-b-1,a,m-1)$ 那么转化式子得到
$$
\begin{aligned}
g(a,b,c,n)
&=\sum_{j=0}^{m-1}\sum_{i=0}^n i[i>t] \
&=\sum_{j=0}^{m-1}\frac{(n+t+1)(n-t)}2 \
&=\frac12 \sum_{j=0}^{m-1} n^2-t^2+n-t \
&=\frac12(mn^2+mn-\sum_{j=0}^{m-1}t^2-\sum_{j=0}^{m-1}t) \
&=\frac12(mn(n+1)-h(N)-f(N))
\end{aligned}
$$
这里引入新的定义
$$
h(a,b,c,n)=\sum_{i=0}^n \lfloor\frac{ai+b}c\rfloor^2
$$

接下来还是表演老两样,一步除去 $a,b$ 中多余部分,令 $N=(a\bmod c,b\bmod c,c,n),A=\lfloor\frac ac\rfloor,B=\lfloor\frac bc\rfloor$
$$
\begin{aligned}
h(a,b,c,n)
&=\sum_{i=0}^n (Ai+B+\lfloor\frac{(a\bmod c)i+(b\bmod c)}c\rfloor)^2 \
&=\sum_{i=0}^n (Ai+B+R)^2 \
&=\sum_{i=0}^n A^2i^2+B^2+R^2+2ABi+2AiR+2BR \
&=\sum_{i=0}^n A^2i^2+2ABi+B^2+h(N)+2Ag(N)+2Bf(N)
\end{aligned}
$$
然后默认 $a<c,b<c$,再一步交换贡献与条件

令 $v=\lfloor\frac{ai+b}c\rfloor,m=\lfloor\frac{an+b}c\rfloor,t=\lfloor\frac{cj+c-b-1}a\rfloor,N=(c,c-b-1,a,m-1)$
$$
\begin{aligned}
h(a,b,c,n)
&=\sum_{i=0}^n\sum_{j=0}^{v-1}(2j+1) \
&=\sum_{j=0}^{m-1}(2j+1)\sum_{i=0}^n[i>t] \
&=\sum_{j=0}^{m-1}(2j+1)(n-t) \
&=nm^2-\sum_{j=0}^{m-1}(2j+1)t \
&=nm^2-2g(N)-f(N)
\end{aligned}
$$
至此我们已经完成了 $g,h$ 的维护,由于这三个函数总是互相调用,因此实现的时候一般是对于同样的状态一起维护,复杂度 $O(\log c)$

万能欧几里得算法

Stern-Brocot 树

这是一种用于维护分数的结构

首先从两个分数 $\frac 01,\frac 10$ 开始,忽略分数的定义我们可以将 $\frac10$ 视为 $\infty$

不断地在序列中的两个分数 $\frac ab,\frac cd$ 中插入 $\frac{a+c}{b+d}$

$$
\frac 01,\frac 11,\frac 10 \
\frac 01,\frac 12,\frac 11,\frac 21,\frac 10 \
\frac 01.\frac 13,\frac 12,\frac 23,\frac 11,\frac 32,\frac 21,\frac 31,\frac 10 \
$$

但是竟然说是树,我们肯定要弄出类似于树的结构来

首先,数学归纳法证明它每一层都是单调的,$\frac ab\leq \frac cd \iff ad\leq bc \iff ad+ab\leq bc+ab \iff \frac ab\leq \frac{a+c}{b+d}$,另一边同理可得证

类似的,我们证明 $bc-ad=1$,$b(a+c)-a(b+d)=bc-ad=1$

将它看做一个不定方程 $bx-ay=1$,因为显然 $c,d$ 为一组解,所以 $c\bot d$,同理 $a\bot b$

这样我们就知道了这棵树每一层都是最简分数


  • 在 Stern-Brocot 树上二分

虽然我们现在有了一个很好的枚举分数的结构,但还是有点不够

考虑不妨考虑这样一个问题,询问小于一个实数 $k$ 的最大的最简分数 $\frac xy$,且满足 $x,y\leq n$

当然这里的小于 $k$ 其实可以改成任意具有单调性的问题,这样这个算法的意义就很大了

首先特判掉 $\frac 01,\frac 10$,因为这两个数实际上不在树中

然后我们每次判断当前节点是否合法,如果合法就往右,否则往左

但是这样还不够,不难发现假如我们想要找到的分数是类似 $\frac n1$ 的数,那么我们可能需要向下走 $O(n)$ 步

不妨简化一些步骤,比如确认下一步的方向后,二分得到往这个方向还会走多少步,这样相当于一段连续路径被 $O(\log n)$ 优化掉了

而对于左右来回切换,不难发现分子分母之和类似于斐波那契数列,增大速度极快,大约也是 $O(\log n)$ 级就无法再走了

也就是说我们的复杂度降到了 $O(\log^2 n)$

容斥与反演

莫比乌斯反演

常见积性/数论函数

积性函数满足若 $x\bot y$ 有 $f(xy)=f(x)*f(y)$

显然一般默认 $f(1)=1$

设 $x=\prod_{i=1}^{cnt} p_i^{b_i},\quad\forall i,b_i \neq 0$

  • $\varepsilon(x)=[x=1]$ 狄利克雷卷积的单位元,完全积性函数
  • $\mu(x)=\begin{cases}0 &\exist b_i>1 \ (-1)^{cnt}& otherwise\end{cases}$
  • $id(x)=x$ 完全积性函数
  • $1(x)=1$ 完全积性函数
  • $\varphi(x)=\sum_{i=1}^x[x\bot i]$
  • $d(x)=\prod_{i=1}^{cnt}(b_i+1)$
  • $\tau(x)=\prod_{i=1}^{cnt}\sum_{j=0}^{b_i}p_i^{j}$

不是积性函数的数论函数 $\lambda(x)=\sum_{i=1}^{cnt} b_i$

定义狄利克雷卷积 $h=g*f$ 表示 $h(n)=\sum_{d\mid n}g(d)f(\frac nd)$

两个积性函数的狄利克雷卷积还是一个积性函数

两个积性函数的点乘 $h(n)=g(n)\cdot f(n)$ 是一个积性函数

贝尔级数

$$
F(z)=\sum_{i\geq1}\frac{f(i)}{i^2}
$$

相当于是为狄利克雷卷积而的诞生的不带有 $x$ 的生成函数
$$
\begin{aligned}
F(z)*G(z)
&=\sum_{i\geq1} \frac{f(i)}{i^2}\sum_{j\geq1}\frac{g(j)}{j^2}\
&=\sum_{n\geq1}\frac{\sum_{d|n}f(d)g(\frac nd)}{n^2}\
&=(F
G)(z)
\end{aligned}
$$
所以这个东西可以表示狄利克雷卷积

而我们一般需要求的是积性函数,所以只需要质数处的值就可以求了

定义质数处的贝尔级数 $F_p(z)=\sum_{n\geq0}f(p^n)z^n$,那么两个积性函数相卷的贝尔级数恰好对应它们在每个质数处的贝尔级数的卷积

当然这里的卷积正确性又有所不同
$$
\begin{aligned}
F_p(z)G_p(z)
&=\sum_{i\geq0}f(p^i)z^i
\sum_{j\geq0}g(p^j)z^j\
&=\sum_{n\geq0}\sum_{i=0}^nf(p^i)g(p^{n-i})z^{n}\
&=\sum_{n\geq0}\sum_{d|n}f(d)g(\frac{p^n}{d})z^n\
&=(F*G)_p(z)
\end{aligned}
$$
下面给出一下常见的贝尔级数

  • $\varepsilon_p(z)=1$
  • $1_p(z)=1+z+z^2+\ldots=\frac1{1-z}$
  • $id_p(z)=1+pz+p^2z^2+\ldots=\frac1{1-pz}$
  • $\mu_p(z)=1-z$
  • $\mu^2_p(z)=1+z$
  • $\varphi_p(z)=1+(p-1)z+p(p-1)z^2+\ldots=1+\frac{(p-1)z}{1-pz}=\frac{1-z}{1-pz}$
  • $d_p(z)=1+2z+3z^2+\ldots=\frac1{(1-z)^2}$

用于推导有很好的效果,但是没有用于计算具体值的价值

莫比乌斯反演核心式

  • $\varepsilon=1*\mu$ 这个主要用于转化掉式子中的艾佛森括号,如 $[i\bot j]$ 等
  • $id=1*\varphi$ 这个主要用于转化掉式子中的比较麻烦的贡献,如 $\gcd(i,j)$ 等
  • $\varphi=\mu*id$

$$
f(n)=\sum_{d\mid n}g(d) \iff g(n)=\sum_{d\mid n}\mu(\frac{n}{d})f(d) \
f(n)=\sum_{n\mid d}g(d) \iff g(n)=\sum_{n\mid d}\mu(\frac{d}{n})f(n)
$$

一些常用的推论式

  1. $\sum_{i=1}^n [i\bot n]i=\frac{\varphi(n)*n}2$

    若 $\gcd(x,n)=1$,由辗转相除法必有 $\gcd(n-x,n)=1$,那么可以将与 $n$ 互质的树两两配对为 $n$

    因为 $\frac n2 \not \bot n$ 所以不会出现重复计算

  2. $\sum_{i=1}^n\sum_{j=1}^n [i\bot j]ij=\sum_{i=1}^n i^2 \varphi(i)$

    考虑递推式

    $$
    \begin{cases}

F(n)=F(n-1)+2\sum_{i=1}^n [i\bot n]in,\quad n\neq 1 \
F(1)=1
\end{cases}
$$

那么展开式子得到 $F(n)=\sum_{i=2}^ni\sum_{j=1}^i [i \bot j]j+1$

代入上一个结论即可

  1. $\sum_{i=1}^n\sum_{j=1}^n [i\bot j]$ 的两种求法

    1. $\mu$ 相关

    $$
    \sum_{i=1}^n \sum_{j=1}^n [i\bot j]=

\sum_{i=1}^n \sum_{j=1}^n \sum_{d\mid i,d\mid j} \mu(d)=\sum_{d=1}^n \mu(d) \lfloor\frac nd\rfloor^2
$$

整除分块即可

  1. $\varphi$ 相关

考虑递推式

$$
\begin{cases}
F(n)=F(n-1)+2\sum_{i=1}^n [i\bot n]=F(n-1)+2\varphi(n) \
F(1)=1
\end{cases}
$$

展开得到 $F(n)=2\sum_{i=1}^n \varphi(i)-1$

  1. $d(xy)=\sum_{i\mid x}\sum_{j\mid y}[i\bot j]$

    $xy$ 的因数是由 $y$ 的所有因数乘上一个 $x$ 的因数的来的

    为防止重复计算,考虑建立一个对应关系 $i*\frac yj$

    那么一个相当于对于这个因数的质因子尽量在 $y$ 中选取,不够就在 $x$ 中再选,多了就除掉,所以不会重复

    也就是一种质因子只会在一个数中出现,而此时这个质因子的次数应该是在这个数之前的所有这个质因子的指数之和加上当前出现的指数

    不难发现这个结论是可以推广的,只需要满足枚举的因子两两互质即可

  2. $\varphi(xy)=\frac{\varphi(x)\varphi(y)\gcd(x,y)}{\varphi(\gcd(x,y))}$

    往下再推导通常是考虑用 $\varphi*1=id$ 来处理掉 $\gcd$ 来简化式子

  3. $\mu(xy)=[x\bot y]\mu(x)\mu(y)$

  4. $f_D(xy)=f_D(x)f_D(y)f_D^2(xy)$ 其中设 $n=\prod_i b_i^{k_i}$ 则 $f_D(n)=\prod_i (-1)^{k_i}[\max{k_i}\leq D]$

    考虑设 $h_D(n)=\max_t{t^{D+1}|n}$

    则 $f_D^2(n)=[h(n)=1]=\sum_{d\mid h_D(n)} \mu(d)=\sum_{d^{D+1}\mid n} \mu(d)$

  5. $2^{f(n)}=\sum_{d\mid n} \mu^2(d)=\sum_{d\mid n}[d\bot \frac nd]$,其中 $f(n)$ 表示 $n$ 的不同质因子个数

    前面的化法看起来比较好看,但实际上求解前缀和时后面的那个式子我推得的复杂度更低,前面的反而优化空间更低

  6. $\sum_{i=1}^n \mu^2(i)=\sum_{i=1}^n\sum_{d^2\mid i}\mu(d)=\sum_{d=1}^{\sqrt n}\mu(d)\lfloor\frac n{d^2}\rfloor$

    考虑 $i$ 无二次质因子的时候,能产生贡献的 $d$ 显然只有 $1$,恰好 $\mu(1)$ 贡献 $1$

    否则 $\mu^2(i)=0$,此时每个次数大于 $1$ 的质因子都会经历选与不选,$\mu(d)$ 相互抵消为 $0$

杜教筛

构造函数 $g(x)$,则有
$$
\sum_{i=1}^n(fg)(i)=\sum_{i=1}^n\sum_{d\mid i}g(d)f(\frac id)=\sum_{i=1}^ng(i)S(\lfloor \frac ni \rfloor)
$$
可得 $g(1)S(n)=\sum_{i=1}^n(f
g)(i)-\sum_{i=2}^ng(i)S(\lfloor\frac ni \rfloor)$

当构造的 $g(x)$ 使 $(f*g)(i)$ 和 $g(i)$ 方便求出时,对 $S(\lfloor \frac ni \rfloor)$ 整除分块求出 $S(n)$

考虑在递归的过程中我们只会求形如 $S(\lfloor\frac nx\rfloor)$ 的式子,那么我们用 map 或哈希表保存已求出的值

那么我们的复杂度就是 $O(\sum_{x=1}^{\sqrt n} \sqrt{\frac nx}+\sqrt x)$,积分近似是 $O(n^{\frac 34})$

考虑线性筛预处理 $S(1)\sim S(T)$,积分得到复杂度是 $O(\frac n{T^{\frac12}}+T)$,取 $T=n^{\frac 23}$ 时有最优复杂度 $O(n^{\frac 23})$

实际 $T$ 可能取大点比较好?线性筛应该比杜教筛常数小

回过来考虑我们记忆化其实不需要使用 map 或哈希表,因为我们已经预处理了 $S(1)\sim S(n^{\frac 23})$

所以我们实际需要保存的是 $\lfloor\frac{n}{x}\rfloor,x\leq n^ \frac 13$,我们直接保留在对应 $\lfloor\frac{n}{\lfloor\frac{n}{x}\rfloor}\rfloor$ 的位置即可

但如果是多组询问的话,使用 map 可能效果更好,因为可以沿用之前的答案


  • 嵌套杜教筛

考虑嵌套调用的过程中查询的位置也是形如 $\lfloor\frac nx\rfloor$ 的位置,所以复杂度不变


  • 杜教筛式算法复合处理

举个例子吧,考虑求解 $\forall x,\sum_{i=1}^{\lfloor\frac nx \rfloor}d(i)$

考虑求解 $\sum_{i=1}^{\lfloor \frac nx \rfloor} d(i)$,枚举一个数看有多少个数包含它这个因子 $\sum_{i=1}^{\lfloor \frac nx \rfloor} \lfloor\frac n{ix}\rfloor$,可以整除分块做到 $O(\sqrt {\lfloor \frac nx \rfloor})$,暴力整除分块套整除分块做完显然是 $O(n^{\frac 34})$

那么我们可以先预处理到一定范围,后面的使用暴力方法硬做,复杂度也是 $O(n^{\frac 23})$

单位根反演

$$
[k\mid n]=\frac 1k\sum_{i=0}^{k-1}\omega_k^{in}
$$

更形象地可以理解为以 $\omega_k^n$ 为步长在圆上走,若整除,则其值为 $1$,走 $k$ 步就是 $k$,否则可以分成若干组,每组都形如 $\omega_t^0+\omega_t^1+\ldots+\omega_t^{t-1}$​​ 等比数列求和可以发现等于 $0$

另外可以特别注意一下 $\omega_2^x=(-1)^x$

拉格朗日反演

若 $F(G(x))=x$,即 $G(F(x))$,称 $F(x)$ 与 $G(x)$ 互为复合逆

此时有
$$
[x^n]F(x)=\frac1nx^{-1}^n=\frac1n [x^{n-1}](\frac x{G(x)})^n \ [x^n]H(F(x))=\frac1n[x^{-1}]H’(x)(\frac1{G(x)})^n=\frac1n [x^{n-1}]H’(x)(\frac x{G(x)})^n
$$

子集反演

$$
f(S)=\sum_{T\subseteq S} g(T) \iff g(S)=\sum_{T\subseteq S} (-1)^{|S|-|T|}f(T) \
f(S)=\sum_{S\subseteq T} g(T) \iff g(S)=\sum_{S\subseteq T} (-1)^{|S|-|T|}f(T)
$$

二项式反演

$$
f(n)=\sum_{i=0}^n \binom{n}{i} g(i) \
g(n)=\sum_{i=0}^n (-1)^{n-i} \binom{n}{i} f(i)
$$

二项式反演是子集反演的特殊情况

多重子集反演

定义 $\mu(S)$ 表示若 $S$ 中有重复元素则 $\mu(S)=0$,否则 $\mu(S)=(-1)^{|S|}$
$$
f(S)=\sum_{T\subseteq S} g(T) \iff g(S)=\sum_{T\subseteq S} \mu(S-T)f(T) \
f(S)=\sum_{S\subseteq T} g(T) \iff g(S)=\sum_{S\subseteq T} \mu(T-S)f(T)
$$
正确性就不证了,通常来说,我们可以强行将任意元素看成互不相同,然后使用子集反演,但是我们也可以根据多重子集反演的 $\mu(S)$ 可能等于 $0$ 来省略掉一些无用情况的枚举

莫比乌斯反演是将一个数的质因子当做多重集的多重子集反演

韦恩图式容斥

给定多个限制,求不满足任何一个限制的总数,令 $S$ 为所有限制的集合
$$
\sum_{T\subseteq S} F(T)*(-1)^{|T|}
$$
本质可以考虑所有方案满足的限制集合,当且仅当一个方案不满足任何一个限制的时候才会贡献 $1$

min-max 容斥

$$
\min(S)=\sum_{T\sub S\ and\ T\neq \empty}(-1)^{|T|-1}\max(T)\
\max(S)=\sum_{T\sub S\ and\ T\neq \empty}(-1)^{|T|-1}\min(T)
$$

这里只证明第一个式子,第二个类似

考虑将 $S$ 集合的元素排序

对于元素 $a_i$ ,它会作为最大值在 $2^{i-1}$ 个子集中贡献

这些子集是完全的,也就是说都会存在,不会因为条件限制不贡献

所以很自然地想到让奇数大小的子集与偶数大小的子集的贡献符号相反
$$
\sum_{i = 1}^n \binom ni =\sum_{i = 0}^n \binom ni ,\quad n\geq1
$$
这样除了最小值只在一个子集中,贡献了一次,其他的元素全部抵消了不贡献

min-max 容斥其实提供了一类容斥的思路,通过对所有子集求值来获得全集的一个值


  • min-max 容斥推广

$$
\text{kthmin}(S)=\sum_{T\sub S\ and\ T \neq \empty} (-1)^{|T|-k}\binom{|T|-1}{k-1} \max(T) \
\text{kthmax}(S)=\sum_{T\sub S \ and\ T \neq \empty} (-1)^{|T|-k}\binom{|T|-1}{k-1} \min(T)
$$

只证明第一个式子,第二个同理

不难发现当 $|T|<k$​ 的时候根本没有贡献,所以比 $a_k$​ 小的数根本不会贡献

考虑研究元素 $a_t$​​ 贡献的系数

$$
\begin{align}
&\sum_{i=1}^t \binom {t-1}{i-1} (-1)^{i-k} \binom{i-1}{k-1} \
&= \sum_{i=1}^t (-1)^{i-k} \binom{t-1}{k-1}\binom{t-k}{i-k} \
&=\binom{t-1}{k-1}\sum_{i=0}^{t-k} (-1)^i \binom{t-k}{i}
\end{align}
$$

与二项式反演类似的思路,求和符号中的内容是奇偶相抵,只有 $t=k$ 的时候系数为 $1$

条件划分容斥

考虑当限制可划分为阶段时,可以通过 dp 优化

设 $dp_i$ 表示最后一个强制不满足的限制是 $i$ 时的方案数

枚举 $i$ 之前的阶段的某个限制 $j$ ,$dp_i=dp_i-dp_j*val(j,i)$,其中 $val(j,i)$ 表示已知不满足限制 $j$,经过 $j\rightarrow i$ 之间的阶段后转移到 $i$ 的方案数变化系数

正确性考虑对于一种不合法方案只在第一个限制处会被算到一次,在之后的每个限制处都不会被算到

斯特林反演

$$
f(n)=\sum_{i=0}^n {n \brace i} g(i) \iff g(n)=\sum_{i=0}^n (-1)^{n-i}{n \brack i} f(i) \
f(k)=\sum_{i=k}^n {i \brace k} g(i) \iff g(k)=\sum_{i=k}^n (-1)^{i-k}{i \brack k} f(i)
$$

证明可以考虑由之后介绍的上升幂,下降幂与常幂的转化推导

一些经典限制的容斥方式

  • 强连通图

将条件改成,缩为 DAG 以后不存在小于 $n$ 的入度为 $0$ 的点

对小于 $n$ 的点集入度为 $0$ 这个限制做容斥即可


  • 元素值不相同

若干个元素,要求它们在值互不相同时满足某个条件

考虑建立一个完全图,一条边表示两个元素的值不能相同

那么一个简单的容斥思路是直接枚举它的子图,然后将边当做要求值相同,然后做无限制的计数

但是枚举子图还是复杂度太大了,最多到 $n=7$ 就不行了

现在我们假设这些元素不区分,也就是我并不关心容斥的时候具体哪两个值相同,只关心分成的联通块分别是怎样的

直接枚举拆分联通块,考虑计算它可能带来的贡献和

令 $G_n=\sum_{S\subseteq E}(-1)^{|S|}=[n=1]$ 表示乱选边的贡献和,令 $F_n$ 表示选的边连成了一个联通块的贡献和

那么 $F_n=G_n-\sum_{i=1}^{n-1} \binom{n-1}{i-1}F_{i}G_{n-i}=(-1)^{n-1}(n-1)!$

多项式

拉格朗日插值

若对于 $n$ 次多项式有 $n+1$ 个点值 $(x_0,y_0),(x_1,y_1),(x_2,y_2)\ldots (x_n,y_n)$

则有
$$
F(x)=\sum_{i=0}^n y_i\prod_{j\neq i}\frac{x-x_j}{x_i-x_j}
$$

通常用于优化背包之类简单的多项式相乘


  • 求解拉格朗日插值的多项式系数

考虑 $\prod_{t=1}^n(x-x_t)$ 对次数来说是背包一样的东西,所以直接做一遍背包,而 $\frac1{x-x_i}$ 可以用逆向背包解决


  • 高维插值

对于一些题目,我们可能需要维护多个未知数,这种情况下同样可以使用拉格朗日插值,且做法基本相同

整数幂和

  • 拉格朗日插值求解

    $\sum_{i=0}^n i^k$ 是关于 $n$ 的 $k+1$ 次多项式,显然可以拉格朗日插值

我们知道在选取的 $x$ 已知的情况下,拉格朗日插值可以 $O(k)$ 得到答案

考虑快速求点值,不难发现 $i^k$ 是一个完全积性函数,在质数位置快速幂计算的复杂度为 $O(\frac k {\log k}*\log k)=O(k)$,线性筛即可

复杂度 $O(k)$,侧重于对固定的 $k$ 快速计算答案


  • 生成函数求解

若已知 $n$,考虑写出 $\sum_{i=1}^n i^k$ 的 EGF,有
$$
\begin{align}
G(x)
&=\sum_{k\geq0}\frac{\sum_{i=1}^ni^k}{k!}x^k \
&=\sum_{i=1}^n\sum_{k\geq0}\frac{i^k}{k!}x^k \
&=\sum_{i=1}^n e^{ix}
\end{align}
$$
由等比数列求和公式得
$$
G(x)=\frac{e^{(n+1)x}-e^x}{e^x-1}
$$
多项式求逆即可

实际实现的时候因为分子分母常数项都为 $0$,所以要先除以 $x$;另外 $g_0$ 显然没有算,所以要自己特判一下

复杂度 $O(n\log n)$,侧重于对于每一个 $k$ 算出同一个 $n$ 的答案

循环卷积

DFT就是对扩域后的多项式做循环卷积 ——Daniel_yuan


  • 求逆

设 $F(x)*F^{-1}(X)=1$

转成点值后不难发现右边是全 $1$

由于 DFT 关于循环卷积的性质,可以将 $F(x)$ 的点值全部取逆再 IDFT 回去得到在以 $lim$ 为长度的循环卷积逆

写成多元方程的形式易证这样的逆是唯一的

bluestein 算法

注意到问题转化需要求 $F_k=\sum_{i=0}^{n-1} a_i w_n^{ik}$

根据组合意义 $ik=\binom{i+k}2-\binom i2-\binom k2$

所以 $F_k=\sum_{i=0}^{n-1}a_iw_n^{\binom{i+k}2-\binom i2-\binom k2}=w_n^{-\binom k2}\sum_{i=0}^{n-1}a_i w_n^{-\binom i2}w_n^{\binom{i+k}2}$ ,显然求和符号内变成了一个卷积形式的东西

也就是说我们通过三次 NTT 实现了任意长度的一次 DFT/IDFT

生成函数推导相关

泰勒展开

$$
F(x)=\sum_{i\geq0}\frac{f^{(i)}(x_0)}{i!}(x-x_0)^i
$$

一般取 $x_0=0$,即麦克劳林级数

广义二项式定理

$$
(x+y)^a=\sum_{i\geq0}\binom{a}{i}x^iy^{a-i},\quad a\in \R \ \binom{a}{i}=\frac{\prod_{j=0}^{i-1}(a-j)}{i!}
$$


  • 拓展:多项式定理

多项式 $(x_1+x_2+\ldots+x_k)^n$ 的展开式中 $x_1^{n_1}x_2^{n_2}\ldots x_k^{n_k}$ 的系数为
$$
\binom{n}{n_1,n_2\ldots,n_k}=\frac{n!}{n_1!n_2!\ldots n_k!}
$$

常用生成函数

$$
\frac1{1-x}=\sum_{i \geq 0}x^i \
e^{x}=\sum_{i\geq0} \frac{1}{i!}x^i \
\ln(1-x)=-\sum_{i\geq1} \frac1{i}x^i
$$

展开生成函数

  • 类 Fibnacci 数列

已知 $f_n=\begin{cases}n&n\leq1\f_{n-1}+f_{n-2}&n>1\end{cases}$

则其生成函数为 $F(x)=x+\sum_{i=2}^\infty f_ix^i$
$$
F(x)=x+\sum_{i=2}^\infty (f_{i-1}*x^{i-1}*x+f_{i-2}*x^{i-2}*x^2)=x+xF(x)+x^2F(x)
$$
解得 $F(x)=\frac{x}{1-x-x^2}$

对分母部分求根 $x_{1,2}=\frac{-1\pm\sqrt5}{2}$

设 $F(x)=\frac{a}{1-\frac{1+\sqrt5}{2}x}+\frac{b}{1-\frac{1-\sqrt5}{2}x}$

通分,待定系数解得 $\begin{cases}a=\frac 1 {\sqrt5} \b=-\frac1{\sqrt(5)}\end{cases}$

两个分式同时展开,$f_n=[x^n]F(x)=\frac1 {\sqrt5}((\frac{1+\sqrt5}2)^n-(\frac{1-\sqrt5}2)^n)$

练习:$f_n=5f_{n-1}-6f_{n-2},n\geq2,f_0=1,f_1=-2$


  • 类 Catalan 数列

已知 $c_n=\begin{cases}1&n=0\\sum_{i=0}^{n-1}c_ic_{n-i-1}&n>0 \end{cases}$

则其生成函数为 $C(x)=c_0+\sum_{i\geq1}c_ix^i$
$$
C(x)=1+\sum_{i\geq1}(\sum_{j=0}^{i-1}c_jc_{i-j-1})x^i=1+x\sum_{i\geq0}(\sum_{j=0}^{i}c_jc_{i-j})x^i=1+xC^2(x)
$$
解得 $C(x)=\frac{1\pm \sqrt{1-4x}}{2x}$

$\because C(0)=c_0=1$

考虑 $\begin{cases}\lim_{x\rightarrow0}C(x)=\frac{1+\sqrt{1-4x}}{2x}=\infty\ \lim_{x\rightarrow0}C(x)=\frac{1-\sqrt{1-4x}}{2x}=1\end{cases}$

得 $C(x)=\frac{1-\sqrt{1-4x}}{2x}$

展开 $\sqrt{1-4x}=\sum_{i\geq0}\binom{\frac12}{i}(-4x)^i$,又有
$$
\begin{align}\binom{\frac12}{i}&=\frac{\frac12*(-\frac12)(-\frac32)\ldots(-\frac{2i-3}2)}{123\ldots*i}\&= (-1)^{i-1}\frac{(2i-3)!!}{2^ii!}\&=(-1)^{i-1}\frac{(2i-2)!}{2^{2i-1}i!(i-1)!},\quad i\in\N^+ \end{align}
$$
则 $C(x)=\sum_{i\geq0}\frac{(2n)!}{n!(n+1)!}x^n$

  • 例:YZOJ3625

五边形数定理

$$
\prod_{i=1}^{\infty}(1-x^i)=\sum_{i=-\infty}^{\infty}(-1)^ix^{\frac{i(3i-1)}{2}}
$$

组合数问题的一个式子

  • $$\binom{n}{k}\times k^{\underline{m}}=\binom{n-m}{k-m}\times n^{\underline{m}}$$

点值转下降幂系数

卷一个 $e^{-x}$

组合数学

排列组合

其实只有组合,虽然也有排列的存在,但一般来说先选出来然后再全排列可能更容易思考

组合计算中,加法往往比减法简单

经典组合恒等式

  1. $\sum_{i=l}^r\binom{i}{x}=\binom{r+1}{x+1}-\binom{l}{x+1}$

    可以结合杨辉三角考虑

  2. $\sum_{i=1}^n\sum_{j=1}^m[i+j=k]\binom ni \binom mj=\binom{n+m}{k}$

    考虑有两个大小分别为 $n,m$ 的集合枚举在两个集合中分别选了多少个

  3. $\binom nm=\frac nm \binom{n-1}{m-1}$

    这个可以直接结合组合数的计算式理解

  4. $\sum_{i=0}^n \binom ni ^2 =\binom{2n}n$

    考虑 $\binom ni=\binom n{n-i}$,那么这条其实就是第 $2$ 条的特殊情况

  5. $\binom nm \binom mk=\binom nk \binom{n-k}{m-k}$

    考虑组合意义就是换了一下选取顺序

经典组合问题

环上邻色不同的染色方案

两种递推式

  1. 容斥 $f(n)=m*(m-1)^{n-1}-f(n-1)$
  2. 讨论 $a_1$ 与 $a_{n-1}$ 是否相同 $f(n)=(m-1)f(n-2)+(m-2)f(n-1)$

通过第二个递推式线性齐次递推可以得到 $f(n)=(m-1)^n+(m-1)(-1)^n$

这个式子使用的时候注意特判 $f_0=0$

错排问题

问题相当于要求计数所以置换环大小都不为 $1$ 的置换数目

那么枚举 $n$ 在置换环上指向的点,然后分类讨论它所在置换环是否为二元环

可得递推式:$f(n)=(n-1)(f(n-1)+f(n-2))$

网格图路径数统计问题
  • $n*m $ 网格图,一条无限制路径:$\binom{n}{n+m}$

  • $nn$ 网格图,一条不穿过对角线路径:$2\frac{\binom{2n}{n}}{n+1}$

  • $n*m $ 网格图($n\leq m$),一条不穿过直线 $y=x$ 路径:$\binom{n+m}{n}-\binom{n+m}{n-1}$

    考虑将第一次越过直线 $y=x$ 后的方向反向,变为 $m+1$ 次向右,$n-1$ 次向上,即求到达 $(n-1,m+1)$ 的走法

  • $n*m$ 网格图,两条不相交路径:$\binom{n+m-2}{n-1}^2-\binom{n+m-2}{n}\binom{n+m-2}{m}$

    考虑两条不相交路径的第一步和最后一步都能确定

    一定是 $(0,1)$ 走到 $(n-1,m)$ 和 $(1,0)$ 走到 $(n,m-1)$

    直接将两种路径的种数乘起来

    然后减去 $(1,0)$ 走到 $(n-1,m)$ 和 $(0,1)$ 走到 $(n,m-1)$ 的方案数

    减去的显然必然是相交路径,考虑将它们第一次相交前的路径交换就能一一对应一种不合法路径,全部减去即可

    当然也可以从 LGV 引理的角度理解

  • $n*m$ 网格图,一条既不穿过 $y=x-c_0$ 也不穿过 $y=x+c_1$ 两条直线的路径,其中 $c_0,c_1 >0$

    同样考虑减去不合法方案

    先往上限对称,再往下限对称,再往上对称,符号为 $-1,1,-1\ldots$ 交替变化

    它表示的是在不合法方案第一次触上限的时候减掉,在这之后第一次触下限加上,以此类推

    类似地做先往下限对称,然后循环

    考虑一种不合法方案触上下限的连续变化序列,不管连续段数量的奇偶性,最终都会减去一次

插板法

将 $n$ 个物品分成 $m$ 组,每组至少一个,答案是 $\binom {n-1}{m-1}$


  • 不定方程解计数

限制 $\sum_{i=1}^n x_i=S$,要求 $x_i\geq R_i$

答案是 $\binom{S-\sum (R_i-1)-1}{n-1}$


  • 单调不下降序列计数

限制 $A\leq x_1 \leq x_2 \leq \cdots \leq x_n \leq B$

考虑研究差分数组,即要求 $\sum_{i=1}^{n+1} y_i=B-A$ 且 $\forall i,y_i\geq 0$

此时转移到不定方程解计数


  • 不定不等式解计数

限制 $\sum_{i=1}^n x_i \leq S$,要求 $x_i\geq 0$

考虑添加一个辅助变量,转移到 $\sum_{i=1}^{n+1} x_i = S$

此时转移到不定方程解计数


  • 组合意义解决特殊贡献形式

组合意义真的是很强的简化问题的办法

举个例子,限制 $\sum_{i=1}^n x_i=S$,要求 $x_i\geq 1$,求每种方法 $\prod_{i=1}^n x_i$ 的和

考虑 $x_i$ 的贡献是 $x_i$ ,从组合意义上可以理解成非空前缀个数

稍微调整一下,拆成两个变量 $a_i,b_i$ 令 $a_i+b_i=x_i+1\quad a_i,b_i\geq 1$ ,此时把 $x_i$ 拆分成两个数的方案数即为 $x_i$ 的贡献

于是原问题简化成把 $S+n$ 个 $1$ 分到 $2n$ 个不能为空的变量中

冒泡下界问题

理论上认为对排列 $p$ 做冒泡排序有一个下界 $\frac 12 \sum_{i=1}^n |i-p_i|$

而一个排列能达到这个下界当且仅当这个排列中不存在长度超过二的下降子序列

这个条件还有另一种表达方式,即可以把它划分成不超过两个上升子序列

这一点由 Dilworth 定理可以理解

这个条件另外还有一种更好的简化方式

可以认为一种合法的前缀 $\max$ 序列对应一个这样的排列

构造方式考虑 $\max$ 序列构成若干相同值的段,把段开头的地方的排列值设定为对应值,其他地方上升地填上剩下的数,得到两个上升子序列

Raney 引理

对于整数数组 $a_1,a_2,\ldots,a_n$,若 $\sum_{i=1}^n a_i=1$,则其所有循环移位中恰有一种满足所有前缀和都为正数


  • 卡特兰数通项推导

$C_n$ 可表示为长为 $2n$ 的合法括号序列的方案数,将左右括号替换成 $\pm 1$ 要求转化为所有前缀和都为非负数

在最前面加上 $+1$,那么问题就转化为了 Raney 引理的形式

因为所有数之和为 $1$,所以不难发现一种序列的不同循环移位一定互不相同

因此一种本质不同的放法一定会被重复计算 $2n+1$ 次,且这些次数中恰有一种符合条件

那么显然答案就是 $\frac{\binom{2n+1}n}{2n+1}=\frac{\binom{2n}n}{n+1}$

斐波那契数

  1. $F_{n-1}F_{n+1}-F_n^2=(-1)^n$

    $$
    \begin{aligned}

F_{n-1}F_{n+1}-F_n^2=F_{n-1}^2+F_n(F_{n-1}-F_n)=-(F_{n-2}F_n-F_{n-1}^2)
\end{aligned}
$$

不断提出 $-1$,式子最终会化到 $F_1F_3-F_2^2$

  1. $\gcd(F_n,F_m)=F_{\gcd(n,m)}$

  2. $\sum_{i=1}^n F_i=F_{n+2}-1$

    对 $n$ 的奇偶性分类讨论

    $\sum_{i=1}^{2n} F_i=\sum_{i=1}^nF_{2i+1}$,添上 $F_2$ 后可以一直推到 $F_{2n+2}$

    $\sum_{i=1}^{2n+1}=F_1+\sum_{i=1}^n F_{2i+2}$,同样添上 $F_2$ 后可以一直推到 $F_{2n+3}$

  3. 在模 $m$ 意义下,类斐波那契数列存在小于等于 $6m$ 的循环节


  • $K$ 阶斐波那契数通项

$$
\begin{cases}
F_{1,0}=1 \
F_{k,n}=F_{k,n-1}+F_{k,n-2}+F_{k-1,n}
\end{cases}
$$

推导生成函数可得
$$
\begin{aligned}
F_{k,n}
&=[x^n]\frac 1{(1-x-x^2)^k} \
&=[x^n]\frac 1{(1-\lambda_1x)^k(1-\lambda_2x)^k}\quad \lambda_{1,2}=\frac{1\pm \sqrt 5}2 \
&=\sum_{i=0}^n \binom{i+k-1}{k-1}\binom{n-i+k-1}{k-1}\lambda_1^i\lambda_2^{n-i} \
&=\frac{\lambda_2^n}{[(k-1)!]^2}\sum_{i=0}^n(i+k-1)^{\underline{k-1}}(n-i+k-1)^{\underline{k-1}}(\frac{\lambda_1}{\lambda_2})^i\
&=\frac{\lambda_2^n(-1)^{k-1}}{[(k-1)!]^2}\sum_{i=0}^n(i+k-1)^{\underline{k-1}}(i-n-1)^{\underline{k-1}}(\frac{\lambda_1}{\lambda_2})^i\
&=\frac{\lambda_2^n(-1)^{k-1}}{[(k-1)!]^2}\sum_{i=0}^n(i+k-1)^{\underline{k-1}}(\frac{\lambda_1}{\lambda_2})^i\sum_{j=0}^{k-1}\binom {k-1}ji^{\underline j}(-n-1)^{\underline{k-1-j}} \
&=\frac{\lambda_2^n(-1)^{k-1}}{[(k-1)!]^2}\sum_{j=0}^{k-1}\binom {k-1}j(-n-1)^{\underline{k-1-j}}\sum_{i=0}^n(i+k-1)^{\underline{k+j-1}}(\frac{\lambda_1}{\lambda_2})^i
\end{aligned}
$$

不难发现我们只需要快速求解后面一部分就可以了
$$
\begin{aligned}
S_y
&=\sum_{i=0}^n (i+x)^{\underline y}c^i \
&=\sum_{i=0}^n (i+x-1)^{\underline y}c^i+y*(i+x-1)^{\underline {y-1}}c^i \
&=cS_{y}-c^{n+1}(n+x)^{\underline y}+(x-1)^{\underline y}\
&+y*(cS_{y-1}-c^{n+1}(n+x)^{\underline{y-1}}+(x-1)^{\underline{y-1}})
\end{aligned}
$$
由上,我们只需要 $O(k)$ 预处理就可做完了

卡特兰数

代表的一些问题

  1. $n$ 个点的有根区分左右儿子的二叉树的不同形态
  2. 圆上 $2n$ 个点匹配使得连边无交点的方案数
  3. $1 \sim n $ 依次入栈对应的不同的出栈序列数

斯特林数

第二类斯特林数

${n \brace m}$ 表示将 $n$ 个不同的元素划分为 $m$ 个互不区分的非空集合的方案
$$
\begin{aligned}
{n \brace m}&={ n-1 \brace m-1}+m*{n-1 \brace m} \
{n \brace m}&=\frac 1 {m!}\sum_{i=0}^m(-1)^i (m-i)^n\binom{m}{i} =\sum_{i=0}^m \frac{(-1)^i*(m-i)^{n}}{i!*(m-i)!} \
n^m&=\sum_{i=0}^n {m \brace i} n^{\underline i}
\end{aligned}
$$
第一个式子是递推式,枚举元素 $n$ 是单独一组还是加入某个集合

第二个式子首先给 $m$ 个集合强行标号,然后容斥有哪些集合为空

第三个式子表示的是将 $m$ 个不区分的球放入 $n$ 个区分的盒子,允许为空的方案数,等号右边相当于在枚举有几个盒子非空

行计算

发现通项公式是一个卷积形式,直接 NTT 即可

列计算

可以写出一个非空集合的 EGF,$F(x)=\sum_{i=1}^\infty \frac{x^i}{i!}$

可以得到 $i \brace m$ 的 EGF 为 $[\frac{x^i}{i!}]\frac{F^k(x)}{k!}$,做一个多项式快速幂即可

第一类斯特林数

${n \brack m}$ 表示将 $n$ 个不同的元素划分为 $m$ 个互不区分的非空圆排列的方案
$$
\begin{aligned}
{n \brack m}&={n-1 \brack m-1}+(n-1)*{n-1 \brack m} \
n!&=\sum_{i=0}^n {n \brack i}
\end{aligned}
$$

第一个递推式的含义就是枚举元素 $n$ 是单独一组还是插在某个圆排列的某个位置

第二个式子右边相当于枚举置换环的个数

行计算

由递推式易得第 $n$ 行的 OGF 可写成 $\prod_{i=0}^{n-1}(i+x)$,其实就是 $x^{\overline{n}}$,这个可以用倍增 NTT 法求解

由 $x^{\overline n}$ 到 $x^{\overline {n+1}}$ 显然直接乘一遍就行了

接下来考虑已知 $x^{\overline{n}}$ 求解 $x^{\overline{2n}}$,拆解式子
$$
x^{\overline{2n}}
=x^{\overline{n}}(x+n)^{\overline{n}}
$$
设 $F(x)=x^{\overline{n}}$,则欲求 $F(x+n)$
$$
\begin{align}
F(x+n)
&=\sum_{i=1}^nf_i(x+n)^i \
&=\sum_{i=1}^nf_i\sum_{j=0}^i\binom{i}{j}x^jn^{i-j} \
&=\sum_{j=0}^nx^j\sum_{i=j}^n f_i\binom{i}{j}n^{i-j} \
&=\sum_{j=0}^nx^j\sum_{i=0}^{n-j} f_{i+j}\frac{(i+j)!}{i!j!}n^i \
&=\sum_{j=0}^{n-1}\frac{x^j}{j!}\sum_{i=0}^{n-j}f_{i+j}(i+j)!*\frac{n^i}{i!}
\end{align}
$$
这又是个类似卷积的形式,只需将一个数组反过来做 NTT 就行了

列计算

仿照第二类斯特林数的求解方法

写出每个非空圆排列的EGF,$F(x)=\sum_{i=1}^\infty \frac{(i-1)!}{i!}x^i=\sum_{i=1}^\infty \frac{x^i}{i}$

然后直接得到 ${i \brack m}$ 的 EGF 为 $\frac{F^k(x)}{k!}$

上升幂与下降幂

首先介绍关于上升幂与下降幂的基本定义与性质
$$
\begin{aligned}
x^{\overline{n}}&=\prod_{i=0}^{n-1}(x+i)=\binom{x+n-1}{n}n! \
x^{\underline{n}}&=\prod_{i=0}^{n-1}(x-i)=\binom{x}{n}n! \
x^{\underline{n}}&=(-1)^n(-x)^{\overline{n}} \
x^{\overline{n}}&=(-1)^n(-x)^{\underline{n}} \
(x+y)^{\overline{n}}&=\sum_{i=0}^n \binom ni x^{\overline i}y^{\overline {n-i}}\
(x+y)^{\underline{n}}&=\sum_{i=0}^n \binom ni x^{\underline i}y^{\underline {n-i}}
\end{aligned}
$$
其中最后两个式子还比较显然,是切换上升幂与下降幂的重要恒等式
$$
\begin{aligned}
x^{\overline{n}}&=\sum_{i=0}^n{n \brack i}x^i \
x^{\underline{n}}&=\sum_{i=0}^n{n \brack i}(-1)^{n-i}x^i \
x^n&=\sum_{i=0}^n{n \brace i}x^{\underline{i}} \
x^n&=\sum_{i=0}^n{n \brace i}(-1)^{n-i}x^{\overline{i}}
\end{aligned}
$$
第一个很容易理解,因为我们已经知道了第一类斯特林数的行计算就是上升幂

第二个就可以根据之前的恒等式由第一个直接推过来

第三个式子是由第二类斯特林数的性质推导来的

第四个式子是根据恒等式由第三个式子推导来的

康托展开

用于求解一个 $n$ 的排列在所有排列中的排名
$$
rank=1+\sum_{i=1}^n\sum_{j=i}^n a_j<a_i!
$$
其实就是计算当前缀相同时,有多少排列会比它小

搭配树状数组可以做到 $O(n\log n)$


  • 逆康托展开

第 $i$ 位的贡献最多为 $(n-i)*(n-i)!<(n-i+1)!$,所以从前往后一位位地考虑显然对的

线性代数

矩阵

矩阵的秩

一个矩阵可以理解为多个向量拼起来,那么消元后最多的线性无关的向量数即为矩阵的秩

也可以简化思考,令 $k$ 阶余子式表示选 $k$ 行 $k$ 列交出的矩阵的行列式

那么矩阵的秩就是最大非零余子式的阶数

给出定理,对称矩阵的秩等于最大非零对称余子式的阶数,这里的对称指的是关于某条斜对角线对称

行列式

矩阵 $A$ 的行列式定义为 $|A|=\sum_{\sigma}(-1)^{sgn(\sigma)}\prod A_{i,\sigma_i}$,$\sigma$ 为枚举一个排列,$sgn(\sigma)$ 为逆序对数

给出一些性质

  1. $\det(A\times B)=\det(A)\det(B)$

  2. $A$ 有逆矩阵 $\iff \det(A)\neq0 \iff A$ 矩阵满秩

    这个比较好理解,从空间变换的角度来说

    非满秩矩阵相当于将空间压缩到更低维度

    从更低维度自然无法找到回到高纬度的逆矩阵

求解方式考虑高斯消元

  1. 行列式某行或某列全为 $0$,行列式为 $0$
  2. 交换行列,行列式取负
  3. 行或列同乘 $k$ ,行列式乘以 $k$
  4. 一行加减另外一行,行列式不变
  5. 上三角行列式为对角线之积
LGV 引理

对一个 DAG 计数

定义 $\omega(P)$ 表示 $P$ 这条路径上的边权之积,$e(u,v)$ 表示 $u$ 到 $v$ 的所有路径 $P$ 的 $\omega(P)$ 之和

定义起点集合 $A$ 和终点集合 $B$​,两个集合的大小都为 $n$

$S$ 表示一组 $A\rightarrow B$​ 的 $n$ 条路径且路径两两没有相交点

建立矩阵
$$
M=
\begin{bmatrix}
e(A_1,B_1)&e(A_1,B_2)&\cdots&e(A_1,B_n) \
e(A_2,B_1)&e(A_2,B_2)&\cdots&e(A_2,B_n) \
\vdots&\vdots&\ddots&\vdots \
e(A_n,B_1)&e(A_n,B_2)&\cdots&e(A_n,B_n)
\end{bmatrix}
$$
则其行列式为所有 $S$ 的 $\omega(P)$ 之积的和

根据行列式定义有 $det(M)=\sum_{S} (-1)^{sgn(\sigma)}\prod_{i=1}^n \omega(S_i)$

其中 $\sigma$ 表示每个起点对于的终点构成的排列的逆序对数,$S_i$ 表示 $S$ 这种不交路径方案中的第 $i$​ 条路径

对比行列式的定义,无交路径方案显然是正常贡献上去了

对于有交路径方案,把第一个交点后的两条路径交换后,对应 $\sigma$ 的奇偶性发生变化,所以所有有交路径方案全部可以一一对应抵消

特殊矩阵行列式
  • 范德蒙德矩阵的行列式

对于如下形式的矩阵
$$
\begin{bmatrix}
1&1&1&\cdots&1 \
x_1&x_2&x_3&\cdots&x_n \
x_1^2&x_2^2&x_3^2&\cdots&x_n^2 \
\vdots&\vdots&\vdots&\ddots&\vdots \
x_1^{n-1}&x_2^{n-1}&x_3^{n-1}&\cdots&x_n^{n-1} \
\end{bmatrix}
$$
它的行列式即为 $\prod_{i<j} (x_i-x_j)$


  • 循环矩阵的行列式

对于矩阵
$$
A=\begin{bmatrix}
a_0&a_1&a_2&\cdots&a_{n-1} \
a_{n-1}&a_0&a_1&\cdots&a_{n-2} \
a_{n-2}&a_{n-1}&a_0&\cdots&a_{n-3} \
\vdots&\vdots&\vdots&\ddots&\vdots \
a_1&a_2&a_3&\cdots&a_0 \
\end{bmatrix}
$$
求解它的行列式时可以给它右乘一个范德蒙德矩阵
$$
V=\begin{bmatrix}
1&1&1&\cdots&1 \
\omega_n^0&\omega_n^1&\omega_n^2&\cdots&\omega_n^{n-1} \
\omega_n^0&\omega_n^2&\omega_n^4&\cdots&\omega_n^{(n-1)2} \
\vdots&\vdots&\vdots&\ddots&\vdots \
\omega_{n}^0&\omega_n^{n-1}&\omega_n^{2(n-1)}&\cdots&\omega_n^{(n-1)(n-1)} \
\end{bmatrix}
$$
定义 $f(x)=\sum_{i=0}^{n-1}a_i x^i$

那么两个矩阵相乘后得到的就是
$$
AV=\begin{bmatrix}
f(\omega_n^0)&f(\omega_n^1)&f(\omega_n^2)&\cdots&f(\omega_n^{n-1}) \
\omega_n^0f(\omega_n^0)&\omega_n^1f(\omega_n^1)&\omega_n^2f(\omega_n^2)&\cdots&\omega_n^{n-1}f(\omega_n^{n-1}) \
\omega_n^0f(\omega_n^0)&\omega_n^2f(\omega_n^1)&\omega_n^4f(\omega_n^2)&\cdots&\omega_n^{(n-1)2}f(\omega_n^{n-1}) \
\vdots&\vdots&\vdots&\ddots&\vdots \
\omega_n^0f(\omega_n^0)&\omega_n^{n-1}f(\omega_n^1)&\omega_n^{2(n-1)}f(\omega_n^2)&\cdots&\omega_n^{(n-1)(n-1)}f(\omega_n^{n-1}) \
\end{bmatrix}
$$
对两边同时求行列式得到
$$
\begin{aligned}
&\det(A)\det(V)=\det(AV)\
&\iff\det(A)\det(V)=\prod_{i=0}^{n-1}f(\omega_n^i)\det(V) \
&\iff\det(A)=\prod_{i=0}^{n-1}f(\omega_n^i)
\end{aligned}
$$

矩阵的逆

形式化地考虑,我们是要求一个矩阵 $A^{-1}$ 使得 $AA^{-1}=A^{-1}A=I$

也就是 $\sum_{i,j,k} A_{i,j} A^{-1}{j,k}=I{i,k}$

定义初等行变换为

  1. 在模意义下给一行乘上一个非 $0$ 数
  2. 把一行加到另一行
  3. 交换两行

发现对矩阵 $A$ 的初等行变换会在相同位置以相同形式和值作用在矩阵 $I$ 上

那么我们直接将 $A_{i,j}$ 通过初等行变换消成 $I$,同时对 $I$ 做同样的事情,那么式子就变成了 $IA^{-1}=B$

这个 $B$ 就是我们要求的逆矩阵

矩阵树定理

无向图生成树计数

设一条边 $(x,y)$ 表示为向量,如 $(2,4)$ 表示为 $\left[ \begin{matrix}0 \ +1 \0 \ -1\end{matrix}\right]$ 或 $\left[\begin{matrix}0 \ -1 \0\+1\end{matrix}\right]$

考虑一些边无法组成环用线性代数的形式描述,则是这些边代表的向量线性无关

将 $m$ 个向量相接得到一个 $n*m$ 的矩阵,考虑这个矩阵的秩为 $n-1$,所以可抹去最后一行

则问题转换为了从 $m$ 个向量中选 $n-1$ 个使它们线性无关的方案数

考虑这 $n-1$ 个向量组成的 $(n-1)*(n-1)$ 的矩阵

当它们线性相关时,这个矩阵的行列式值为 $0$

当它们线性无关时,这个矩阵的行列式值为 $+1$ 或 $-1$

所以可求,每个这样的矩阵的行列式的值的平方之和

设图矩阵为 $D$,它的置换 $D^T$,可知我们所求为 $\sum_{S\subseteq T,|S|=n-1}\det(D_S)\det({D^T}_S)$

Binet-Cauthy公式

对于 $nm$ 的矩阵 $A$ 和 $mn$ 的矩阵 $B$

$\det(AB)=\sum_{S\subseteq U,|S|=n}\det(A_S)\det(B_S)$

其中 $U={1,2,\ldots,m}$

$A_S$ 表示 $A$ 中行标号集合为 $S$ 的列组成的矩阵

$B_S$ 表示 $B$ 中列标号集合为 $S$ 的行组成的矩阵

根据 Binet-Cauthy 公式转换为求 $\det(DD^T)$

此时复杂度为 $O(n^2m)$,需要消去复杂度中的 $m$

考虑 $DD^T$ 矩阵的现实意义

  • ${DD^T}_{i,i}$:点 $i$ 的度数
  • ${DD^T}_{i,j}$:$i$ 与 $j$ 之间的边数的相反数

此时复杂度为 $O(n^3)$

将重边数抽象成边权,则可理解为对角线上对应点加上所连边边权减去邻接矩阵

有向图生成树计数

消去的一行一列为根,处理对角线后减去邻接矩阵

  • 内向树:起点对应位置加值
  • 外向树:终点对应位置加值

多项式拓展

  • 化乘为加

将矩阵的元素换成多项式即可

例:[省选联考 2020 A 卷] 作业题


  • 控制边权 $1$

边权分 $01$,控制 $0$ 权边的数目恰好为 $K$

将 $0$ 权边边权设为 $x$,$1$ 权边边权设为 $1$

维护求出的多项式的第 $K$ 次方的系数即可

多项式不太好处理,但是它最多为 $n-1$ 次,那么可以直接插值


  • 控制边权 $2$

边权分 $a,b,c,d$,要求 $a$ 的数目不少于 $b$,$c$ 的数目不少于 $d$

令 $a$ 为 $x$,$b$ 为 $x^{-1}$,$c$ 为 $y$,$d$ 为 $y^{-1}$,计算最后所有 $x^iy^j,i,j\geq 0$

二阶插值即可

Best 定理

用于求解含有欧拉回路的有向图的不同欧拉回路个数
$$
ans=tree_s \prod_{i=1}^n (deg_i-1)!
$$
其中 $tree_s$ 表示以 $s$ 为根的有向生成树个数,内外向都可以,$s$ 也可以任选

这里以内向生成树证明,外向显然一样

考虑当一个点上所有边都被经过以后可以理解为这个点被删掉了,令 $s$ 最后被删

那么根据每一个点最后走的出边可以建出一棵树,不同的树的个数也就是 $tree_s$

剩下的边任意选择走的顺序都可对应一种方案,这里不详细证明

另外虽然 $s$ 没有树边,剩下的出边应该有 $deg_s$ 个,但是因为出现循环同构,所以只用乘 $(deg_s-1)!$

要注意的是,Best 定理只适用于至少包含一条边的有欧拉回路的图

只有一个点没有边和不存在欧拉回路的情况都要特判

概率与期望

概率

概率的基本计算公式:$P(A)=\frac{n(A)}{n(all)}$

  • 对于互不相容的事件 $x_1,x_2\ldots x_n$,发生其中任意一个的概率为 $\sum_{i=1}^n p_i$

  • 对于互相独立的事件 $x_1,x_2\ldots x_n$,这些事件全部发生的概率为 $\prod_{i=1}^n p_i$

互不相容指两件事不能同时发生,互相独立指是否发生一件事不会影响另一件事发生的概率

  • $P(A\cup B)=P(A)+P(B)-P(A\cap B)$ (可根据融斥原理推至多个事件求并的概率)

  • $P(A-B)=P(A)-P(A\cap B)$

  • 全概率公式:若 $\sum_i P(A_i)=1$,且 $A_i$ 互不相容,则 $P(B)=\sum_i P(A_i)*P(B|A_i)$

  • 条件概率:$P(A|B)=\frac {P(A\cap B)}{P(B)}$

    考虑恒等式 $P(A\cap B)=P(A|B)P(B)$

    移项即可

  • 贝叶斯公式:若 $\sum_i P(C_i)=1$,且 $C_i$ 互不相容,$P(A\mid B)=\frac{P(A\cap B)}{\sum_i P(B|C_i)}$

    考虑把条件概率的分母用全概率公式替换即可

期望

期望表示各种情况下的加权平均

*期望的基本计算公式:$E(X)=\sum_i P(x_i)E(x_i)$

  • 全期望公式:$E(X)=E(E(X\mid Y))$

    $E(E(X\mid Y))=\sum_{i} E(X\mid Y_i)P(Y_i)=\sum_i\sum_j X_jP(Y_i)P(X_j\mid Y_i)=\sum_i\sum_j X_j P(X_j\cap Y_i)=\sum_j X_j P(X_j)=E(X)$

期望方程

  • 抛硬币 $1$

一枚硬币,抛到正面的概率为 $p$,问期望几次得到一个正面

设抛到正面的期望为 $E$

则可得方程
$$
\begin{align}
&E=p1+(1-p)(E+1) \
&\iff E=\frac1p
\end{align}
$$


  • 抛硬币 $2$

不断抛一枚均匀的硬币,知道连续两次正面朝上,求期望抛的次数

设已连续抛到 $0$ 个正面的期望为 $E_0$,已连续抛到 $1$ 个正面的期望为 $E_1$

则可得方程组
$$
\begin{align}
&\begin{cases} E_0=\frac12*(E_1+1)+\frac12*(E_0+1) \ E_1=\frac121+\frac12(E_0+1) \end{cases} \
&\iff \begin{cases} E_0=6 \ E_1=4 \end{cases}
\end{align}
$$

期望 dp

当期望方程中值的转移是无环的时,可直接 dp

循环转移

列出方程,直接高斯消元

另外当方程中的环只受少量(通常是一个)特定未知数的影响时,可直接维护每个未知数用特定未知数表示时的系数,此时复杂度为 $O(n)$

command_block 博客 (技巧)

  • 多项式计数杂谈
  • 炫酷反演魔术

待补充

  • PAM高级玩法
  • 一类数论函数的筛法
  • 线性基全家桶
  • 博弈论结论