数论
最大公约数
gcd
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
lcm
long long lcm(int a, int b) { return 1ll * a / gcd(a, b) * b; }
线性筛
从小到大枚举因子
p[i] : i的最小素因子
prime[i]:素数的值
void init(int n) {
p[1] = 1;
for(int i = 2; i <= n; i ++) {
if(!p[i]) p[i] = i, prime[++tot] = i;
for (int j = 1; j <= tot && prime[j] * i <= n; j ++) {
p[i * prime[j]] = prime[j];
if(p[i] == prime[j]) break;
}
}
}
扩展欧几里得
用来求解不定方程
a
x
+
b
y
=
g
c
d
(
a
,
b
)
ax+by=gcd(a,b)
ax+by=gcd(a,b)
int exgcd(int a, int b, int &x, int &y) {
if(b == 0) {
x = 1;
y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= (a / b) * x;
return d;
}
求解非负整数解(x, y),输出x最小的解
a
x
+
b
y
=
d
ax+by=d
ax+by=d
cin >> a >> b >> m;
ll d = exgcd(a, b, x, y);
if(m % d != 0) {
cout << -1 << "\n";
continue;
}
a /= d; b /= d; m /= d;
__int128 xx = (__int128)x * m;
xx %= b;
if (xx < 0) xx += b;
__int128 yy = (m - a * xx) / b;
if(yy < 0) {
cout << -1 << "\n";
continue;
}
cout << (ll)xx << " " << (ll)yy << "\n";
算数基本定理
任何一个大于1的自然数N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
N
=
p
1
e
1
∗
p
2
e
2
∗
p
3
e
3
.
.
.
∗
p
n
e
n
(
p
1
<
p
2
<
p
3
<
.
.
.
<
p
n
)
N={p_{1}}^{e_{1}}*{p_{2}}^{e_{2}}*{p_{3}}^{e_{3}}...*{p_{n}}^{e_{n}} (p_{1} < p_{2} < p_{3} < ...<p_{n})
N=p1e1∗p2e2∗p3e3...∗pnen(p1<p2<p3<...<pn)
欧拉函数
对正整数n,欧拉函数是小于n的正整数中与n互质的数的数目。
φ
(
x
)
=
x
∗
∏
i
=
1
n
(
1
−
1
p
i
)
\varphi (x) = x*\prod_{i = 1}^{n}(1-\frac{1}{p_{i}})
φ(x)=x∗i=1∏n(1−pi1)
int phi(int n) {
int phin = n, res = n;
for(int i = 2; i * i <= n; i++) {
if(n % i == 0) {
phin = phin / i * (i - 1);
while(n % i == 0) n /= i;
}
}
if(n > 1) phin = phin / n * (n - 1);
return phin;
}
欧拉定理
设a,m都为正整数,且gcd(a,m) = 1,则有
a
φ
(
m
)
≡
1
(
m
o
d
m
)
a^{\varphi (m)}\equiv 1(mod\ m)
aφ(m)≡1(mod m)
逆元
任意整数a和其逆元满足
a
a
−
1
≡
1
(
m
o
d
n
)
aa^{-1}\equiv 1(mod\ n)
aa−1≡1(mod n)
在对除法运算进行取模时,利用逆元有 (a / b) mod p = a * inv(b) (mod p).
当a为b的倍数时,有
a
÷
b
m
o
d
p
=
(
a
m
o
d
(
b
×
p
)
)
÷
b
a÷bmodp=(amod(b×p))÷b
a÷bmodp=(amod(b×p))÷b
费马小定理求逆元(模数为素数时)
long long quickpow(long long a, long long b) {
if (b < 0) return 0;
long long ret = 1;
a %= mod;
while(b) {
if (b & 1) ret = (ret * a) % mod;
b >>= 1;
a = (a * a) % mod;
}
return ret;
}
long long inv(long long a) {
return quickpow(a, mod - 2);
}
扩展欧几里得求逆元
int getInv(int a, int mod) {
int x, y;
int d = exgcd(a, mod, x, y);
return d == 1 ? (x % mod + mod) % mod : -1;
}
欧拉定理同样可以求逆元
求解1到n的逆元
inv[1] = 1;
for(int i = 2; i <= n; i++) {
inv[i] = (p - p / i) * inv[p % i] % p;
}
求解阶乘逆元
void init()
{
fac[0] = 1;
for (int i = 1; i < maxn; i++)
{
fac[i] = fac[i - 1] * i % mod;
}
inv[maxn - 1] = quick_pow(fac[maxn - 1],mod - 2,mod);
for (int i = maxn - 2; i >= 0; --i)
{
inv[i] = inv[i + 1] * (i + 1) % mod;
}
}
求n个数的逆元
s[0] = 1;
for (int i = 1; i <= n; i++) s[i] = s[i - 1] * a[i] % p;
int x, y;
exgcd(s[n], p, x, y);
if (x < 0) x += p;
t[n] = x;
assert(s[n] * x % p == 1);
for(int i = n; i >= 1; i --) t[i - 1] = t[i] * a[i] %p;
for(int i = 1; i <= n; i++) {
inv[i] = s[i - 1] * t[i] % p;
}
中国剩余定理
整除分块
for(ll l = 1; l <= n; l ++) {
ll d = n / l, r = n / d;
sum += (r - l + 1) * d;
// l .. r n / x = d
l = r;
}
long long 取模
ll mul(ll x, ll y, ll m) {
x %= m; y %= m;
ll d = ((long double) x * y / m);
d = x * y - d * m;
if (d >= m) d -= m;
if(d < 0) d += m;
return d;
}
Lucas定理
long long Lucas(long long n, long long m, long long p) {
if(m == 0) return 1;
return (c(n % p, m % p, p) * Lucas(n / p, m / p, p)) % p;
}
扩展欧拉定理
a b % m = a b % φ ( m ) + φ ( m ) % m , g c d ( a , m ) ! = 1 & b > = ϕ ( m ) a b % m = a b % m , g c d ( a , m ) ! = 1 & b < ϕ ( m ) a b % m = a b % ϕ ( m ) % m , g c d ( a , m ) = 1 a^{b}\ \% \ m = a^{b \% \ \varphi(m) + \varphi(m)}\ \%\ m\ , \ gcd(a,m)!=1\&b>=\phi(m) \\a^{b}\%m=a^b\%m\ ,\ gcd(a,m)!=1\&b<\phi(m) \\a^{b}\%m=a^{b\%\phi(m)}\%m, \ gcd(a,m)=1 ab % m=ab% φ(m)+φ(m) % m , gcd(a,m)!=1&b>=ϕ(m)ab%m=ab%m , gcd(a,m)!=1&b<ϕ(m)ab%m=ab%ϕ(m)%m, gcd(a,m)=1
积性函数
定义:(a,b) = 1, f(ab) = f(a)f(b)
常见的积性函数
i
d
(
x
)
=
x
id(x) = x
id(x)=x
2.
1
(
x
)
=
1
1(x) = 1
1(x)=1
3.
e
(
x
)
=
{
1
x
=
1
0
x
≠
1
e(x) = \left\{\begin{matrix} 1& x = 1 & \\ 0& x\neq 1 & \end{matrix}\right.
e(x)={10x=1x=1
4.欧拉函数
φ
(
n
)
=
n
∑
p
∣
n
(
1
−
1
p
)
\varphi (n) = n\sum_{p | n}(1-\frac{1}{p})
φ(n)=np∣n∑(1−p1)
5.d(n)因子个数
d
(
p
e
)
=
e
+
1
d(p^{e}) = e + 1
d(pe)=e+1
6.
σ
(
p
e
)
=
p
0
+
p
1
+
.
.
.
+
p
e
\sigma (p^{e}) = p^{0} + p^{1} + ...+p^{e}
σ(pe)=p0+p1+...+pe
7.莫比乌斯函数
μ
(
p
e
)
=
{
1
e
=
0
−
1
e
=
1
0
e
≥
2
\mu (p^{e})=\left\{\begin{matrix} 1 & e=0 \\ -1 & e=1\\ 0&e\geq 2 \end{matrix}\right.
μ(pe)=⎩
⎨
⎧1−10e=0e=1e≥2
性质
∑
d
∣
n
μ
(
d
)
=
[
n
=
1
]
\sum_{d|n}\mu(d)=[n=1]
d∣n∑μ(d)=[n=1]
线性筛求积性函数
由
f
(
n
)
=
f
(
p
1
e
1
)
∗
f
(
p
2
e
2
)
∗
.
.
.
∗
f
(
p
k
e
k
)
f(n) = f(p_{1}^{e_{1}})*f(p_{2}^{e_{2}}) *...*f(p_{k}^{e_{k}})
f(n)=f(p1e1)∗f(p2e2)∗...∗f(pkek)
得
f
(
n
)
=
f
(
p
1
e
1
)
∗
f
(
n
/
p
1
e
1
)
f(n) = f(p_{1}^{e_{1}})*f(n/p_{1}^{e_{1}})
f(n)=f(p1e1)∗f(n/p1e1)
const int N = 2e7 + 1000;
int p[N], pr[N / 5], n, pe[N], tot;
int f[N], a, b, ans;
void prime() {
p[1] = 1;
for(int i = 2; i <= n; i ++) {
if(!p[i]) p[i] = i, pe[i] = i, pr[++tot] = i;
for(int j = 1; j <= tot && pr[j] * i <= n; j ++) {
p[i * pr[j]] = pr[j];
if (p[i] == pr[j]) {
pe[i * pr[j]] = pe[i] * pr[j];
break;
} else {
pe[i * pr[j]] = pr[j];
}
}
}
}
void compute(int n, function<void(int)> calcpe) {
f[1] = 1;
for(int i = 2; i <= n; i ++) {
if(i == pe[i]) calcpe(i);
else f[i] = f[pe[i]] * f[i / pe[i]];
}
}
//因子个数
compute(n, [&](int x){
f[x] = f[x / p[x]] + 1;
});
//因子和
compute(n, [&](int x){
f[x] = f[x / p[x]] + x;
});
//欧拉函数
compute(n, [&](int x){
f[x] = x / p[x] * (p[x] - 1);
});
//mo'b
compute(n, [&](int x){
f[x] = x == p[x] ? -1 : 0;
});
不同质因子个数
求一个数不同的质因子个数 p[i]。(加性函数)
void init(int n) {
p[1] = 1;
for(int i = 2; i <= n; i ++) {
if(!vis1[i]) p[i] = 1, prime[++tot] = i;
for (int j = 1; j <= tot && prime[j] * i <= n; j ++) {
vis1[i * prime[j]] = 1;
if(i % prime[j] == 0) {
p[i * prime[j]] = p[i];
break;
}
p[i * prime[j]] = p[i] + p[prime[j]];
}
}
}
迪利克雷卷积
定义
h ( n ) = ∑ d ∣ n f ( d ) g ( n / d ) = ∑ d 1 d 2 = n f ( d 1 ) g ( d 2 ) h(n) = \sum_{d|n}f(d)g(n/d) = \sum_{d_{1}d_{2} = n}f(d_{1})g(d_{2}) h(n)=d∣n∑f(d)g(n/d)=d1d2=n∑f(d1)g(d2)
常见的卷积
1.d(n) 因子个数
d
(
n
)
=
∑
d
∣
n
1
(
d
)
1
(
d
/
n
)
d(n) = \sum_{d|n}1(d)1(d/n)
d(n)=d∣n∑1(d)1(d/n)
2.因子和
σ
(
n
)
=
∑
d
∣
n
i
d
(
d
)
1
(
n
/
d
)
\sigma(n) = \sum_{d|n}id(d)1(n/d)
σ(n)=d∣n∑id(d)1(n/d)
3.
f
=
f
∗
e
f = f*e
f=f∗e
4.
e
=
1
∗
μ
e = 1*\mu
e=1∗μ
性质
1.交换律
h
=
f
∗
g
=
g
∗
f
h = f*g = g*f
h=f∗g=g∗f
2.结合律
p
=
(
f
∗
g
)
∗
h
=
f
∗
(
g
∗
h
)
p=(f*g)*h=f*(g*h)
p=(f∗g)∗h=f∗(g∗h)
3.f和g是积性函数,则f*g也是积性函数
莫比乌斯反演
形式
f ( n ) = ∑ d ∣ n g ( d ) < = > g ( n ) = ∑ μ ( n / d ) f ( d ) f(n) = \sum_{d|n}g(d) <=>g(n) = \sum \mu(n/d)f(d) f(n)=d∣n∑g(d)<=>g(n)=∑μ(n/d)f(d)
f = g ∗ 1 < = > g = f ∗ u f = g*1 <=>g=f*u f=g∗1<=>g=f∗u
一些经典的反演
在求
∑
i
=
1
n
∑
j
=
1
m
[
g
c
d
(
i
,
j
)
=
1
]
(
n
<
m
)
\sum^{n}_{i=1}\sum^{m}_{j=1}[gcd(i,j)=1](n<m)
i=1∑nj=1∑m[gcd(i,j)=1](n<m)
时,可以通过上式变化为化简得
该式可以通过整除分块O(根号n)求解
形如同样可以通过上述方法求解
code如下:
//P3455
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e6 + 1000;
int p[N], pr[N / 5], n, pe[N], tot;
int f[N], smu[N], a, b, ans;
void prime() {
p[1] = 1;
for(int i = 2; i <= n; i ++) {
if(!p[i]) p[i] = i, pe[i] = i, pr[++tot] = i;
for(int j = 1; j <= tot && pr[j] * i <= n; j ++) {
p[i * pr[j]] = pr[j];
if (p[i] == pr[j]) {
pe[i * pr[j]] = pe[i] * pr[j];
break;
} else {
pe[i * pr[j]] = pr[j];
}
}
}
}
void compute(int n, function<void(int)> calcpe) {
f[1] = 1;
for(int i = 2; i <= n; i ++) {
if(i == pe[i]) calcpe(i);
else f[i] = f[pe[i]] * f[i / pe[i]];
}
}//莫比乌斯函数求解
int main() {
int q;
cin >> q;
n = 5e4 + 10;
prime();
compute(n, [&](int x){
f[x] = x == p[x] ? -1 : 0;
});
for(int i = 1; i <= n; i ++) {
smu[i] = smu[i - 1] + f[i];
}
while(q --) {
int a, b, k;
cin >> a >> b >> k;
a /= k, b /= k;
if(a > b) swap(a, b);
ll ans = 0;
for(int l = 1; l <= a; l ++) {
int r = min(a / (a / l), b / (b / l));
ans += (1ll*smu[r] - smu[l - 1]) * (a / l) * (b / l);
l = r;
// cout << l << endl;
}//整除分块
cout << ans << endl;
}
}
BSGS
a和m互质,求解最小的非负整数t,使得下式成立
a
t
≡
b
(
m
o
d
m
)
a^t \equiv b(\bmod m)
at≡b(modm)
由欧拉定理
a
φ
(
m
)
≡
1
(
m
o
d
m
)
a^{\varphi(m)} \equiv 1(\bmod m)
aφ(m)≡1(modm)
说明
a
t
a^t
at在模m的意义下有一个长度为
φ
(
m
)
\varphi(m)
φ(m)的循环节,所以我们只需要考虑
t
≤
φ
(
m
)
t \leq \varphi(m)
t≤φ(m)的情形就可以了。当m是质数时
φ
(
m
)
=
m
−
1
\varphi(m)=m-1
φ(m)=m−1所以暴力枚举的复杂度为O(m)。
考虑优化,我们把 t t t拆成 k x − y kx-y kx−y, k = m + 1 k=\sqrt m + 1 k=m+1, x ∈ [ 1 , k ] x \in[1, k] x∈[1,k], y ∈ [ 0 , k − 1 ] y \in [0,k-1] y∈[0,k−1]
当 x = 1 , y = k − 1 x=1,y=k-1 x=1,y=k−1时, t = 1 t=1 t=1; 当 x = k , y = 0 x=k,y=0 x=k,y=0时, t = k 2 t=k^2 t=k2 ;这样我们通过遍历x和y,可以让t遍历完 [ 1 , m ] [1,m] [1,m]。
用x,y替换t后 a k x − y ≡ b ( m o d m ) a^{kx-y}\equiv b(\bmod m) akx−y≡b(modm),移项可得, a k x ≡ b ∗ a y ( m o d m ) a^{kx} \equiv b*a^{y}(\bmod m) akx≡b∗ay(modm)。我们可以预处理出 b y ( m o d m ) b^y(\bmod m) by(modm) 的所有值对应的最大的y。然后枚举x,看是否有对应的y和它相等。复杂度为 O ( m ) O(\sqrt{m}) O(m)
cin >> m >> a >> b;
int k = sqrt(m) + 1;
if(b == 1) {
cout << 0;
return 0;
}
map<int, int> vis;
int now = 1;
for(int y = 0; y <= k - 1; y ++) {
vis[(now * b) % m] = y;
now = 1ll * now * a % m;
}
int cur = now;
for(int x = 1; x <= k; x ++) {
if(vis.find(now % m) != vis.end()) {
int y = vis[now % m];
if(k * x - y >= 0) ans = min(ans, k*x-y);
}
now = 1ll * now * cur % m;
}
高斯消元
异或高斯消元
int xor_gauss() {
int r = 1;
for (int c = n; c >= 1; c--) {
int temp = r;
for (int i = r; i <= n; i++)
if (a[i][c]) {
temp = i;
break;
}
if (!a[temp][c]) continue;
swap(a[r], a[temp]);
for (int i = r + 1; i <= n; i++) {
if (a[i][c]) a[i] ^= a[r];
}
r++;
}
if (r <= n) {
for (int i = 1; i <= n; i++) {
if (a[i] == 1) return -1;
if (a[i] == 0) return quick_pow(2, n - i + 1, mod);
}
}
for (int i = 1; i <= n; i++) {
int res = a[i][0];
for (int j = i - 1; j; j--)
res ^= a[i][j] ^ ans[j];
ans[i] = res;
}
return 1;
}
高斯消元求行列式 (取模)
int gauss(int a[N][N],int n)
{
int res=1;
for(int i=1; i<=n; i++)
{
for(int j=i+1; j<=n; j++)
{
while(a[j][i])
{
int t=a[i][i]/a[j][i];
for(int k=i; k<=n; k++)
a[i][k]=(a[i][k]-t*a[j][k]+mod)%mod;
swap(a[i],a[j]);
res=-res;
}
}
res=(res*a[i][i])%mod;
}
return (res+mod)%mod;
}
图论
邻接表建图
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
树
直径
求树的直径并输出
两次dfs(边权不能为负)
vector<LL> d(N);
vector<int> p(N);
function<void(int, int, LL)> dfs = [&](int u, int par, LL dep) {
d[u] = dep;
p[u] = par;
for (auto [v, w] : G[u]) if (v != par) {
dfs(v, u, dep + w);
}
};
dfs(0, -1, 0);
int r = max_element(d.begin(), d.end()) - d.begin();
dfs(r, -1, 0);
vector<int> v;
int s = max_element(d.begin(), d.end()) - d.begin();
for (int x = s; x >= 0; x = p[x]) v.push_back(x);
cout << d[s] << " " << v.size() << "\n";
for (int x : v) cout << x << " ";
LCA
倍增求LCA
int fa[N][16]
void bfs() {
memset(depth, 0x3f, sizeof depth);
depth[root] = 1, depth[0] = 0;
int hh = 0, tt = -1;
q[++tt] = root;
while(hh <= tt) {
int t = q[hh ++];
for(int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if(depth[j] > depth[t] + 1) {
depth[j] = depth[t] + 1;
q[++tt] = j;
fa[j][0] = t;
for(int k = 1; k <= 15; k++)
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
int lca(int a, int b) {
if(depth[a] < depth[b]) swap(a, b);
for(int k = 15; k >= 0; k--)
if(depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if(a == b) return a;
for(int k = 15; k >= 0; k --)
if(fa[a][k] != fa[b][k]) {
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
树上差分
点差分
将两点u,v之间路径上的所有点权增加x, o = LCA(u, v), o的父亲结点为p,则:
d
i
f
f
[
u
]
+
=
x
,
d
i
f
f
[
v
]
+
=
x
,
d
i
f
f
[
o
]
−
=
x
,
d
i
f
f
[
p
]
−
=
x
;
diff[u]+=x,diff[v]+=x,diff[o]-=x,diff[p]-=x;
diff[u]+=x,diff[v]+=x,diff[o]−=x,diff[p]−=x;
code
void dfs(int u, int fa) {
int res = 0;
for(int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if(j == fa) continue;
dfs(j, u);
res += sum[j];
}
sum[u] = res + diff[u];
}
cin >> u >> v;
int x = lca(u, v);
diff[u] += 1;
diff[v] += 1;
diff[x] -= 1;
diff[p[x]] -= 1;
边差分
将两点u, v之间路径上的边权增加x,o = LCA( u,v),以每条边两端深度较大的节点存储改边的差分数组,则操作如下:
d
i
f
f
[
u
]
+
=
x
,
d
i
f
f
[
v
]
+
=
x
,
d
i
f
f
[
o
]
−
=
2
∗
x
;
diff[u]+=x,diff[v]+=x,diff[o]-=2*x;
diff[u]+=x,diff[v]+=x,diff[o]−=2∗x;
最短路
floyd
传递闭包
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
d[i][j] |= d[i][k] & d[k][j];
Tarjan缩点
有向图的强连通分量
连通分量:对于分量中任意两点u,v必然可以从u走到v,且从v走到u
强连通分量:极大连通分量
通过Tarjan缩点可以让有向图转变为有向无环图,转变后的图里面的每一个点是原图的一个强连通分量。
一个有向图,变成一个强连通分量至少需要添加max(p,q)条边,p为缩点后入度为0的点,q为缩点后出度为0的点
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, size[N];
void tarjan(int u) {
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top ] = u, in_stk[u] = 1;
for(int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if(!dfn[j]) {
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if(in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if(dfn[u] == low[u]) {
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = 0;
id[y] = scc_cnt;
size[scc_cnt] ++;
} while(y != u);
}
}
差分约束
for(int i = 1; i <= m; i++) {
int f, x, y;
std::cin >> f >> x >> y;
if(f == 1) { // 等于 x == y, x -> y, w = 0, y -> x, w = 0;
add(h,x,y,0);add(h,y,x,0);
} else if(f == 2) { // 小于 x < y, x->y, w = 1;
add(h,x,y,1);
} else if(f == 3) { // 大于等于 x >= y, y->x, w = 0;
add(h,y,x,0);
} else if(f == 4) { // 大于 x > y, y->x, w = 1;
add(h,y,x,1);
} else if(f == 5) { // 小于等于 x <= y, x->y, w = 0;
add(h,x,y,0);
}
}
for(int i = 1; i <= n; i++) {
add(h,0,i,1);
}
// 缩点
tarjan(0);
// 建DAG
int f = 1;
for(int i = 0; i <= n; i ++) {
for(int j = h[i]; ~j; j = ne[j]) {
int k = e[j];
int u = id[i], v = id[k];
if(u == v) {
// 不符合不等式关系
if(w[j] > 0) {
f = 0;
break;
}
} else add(h1, u, v, w[j]);
}
if(!f) break;
}
if(!f) {
std::cout << "-1\n";
return 0;
}
// DAG求最长路
for(int i = scc_cnt; i >= 0; i --) {
for(int j = h1[i]; ~j; j = ne[j]) {
int k = e[j];
d[k] = std::max(d[i] + w[j], d[k]);
}
}
无向图的双连通分量
边双连通分量 e-Dcc
无向图中,极大的不含有桥的连通块被称为边的双连通分量
在里面不管删掉哪条边,仍然连通。
每对点之间至少存在两条没有公共边的路径
一个无向图变成边双连通分量,至少需要添加(cnt + 1)/ 2 条边,cnt为缩点后度数为1的点的个数
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
int id[N], dcc_cnt; // 每个点所属分量编号
bool is_bridge[M];
void tarjan(int u, int from)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j, i);
low[u] = min(low[u], low[j]);
if (dfn[u] < low[j])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if (i != (from ^ 1))
low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ dcc_cnt;
int y;
do {
y = stk[top -- ];
id[y] = dcc_cnt;
} while (y != u);
}
}
点双连通分量 v-Dcc
极大的不包含割点的连通块被称为点的双连通分量
每个割点至少属于两个点双连通分量
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N]; // 每个分量有哪些点
bool cut[N]; // 是否为割点
int root;
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u;
if (u == root && h[u] == -1)
{
dcc_cnt ++ ;
dcc[dcc_cnt].push_back(u);
return;
}
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (dfn[u] <= low[j])
{
cnt ++ ;
if (u != root || cnt > 1) cut[u] = true;
++ dcc_cnt;
int y;
do {
y = stk[top -- ];
dcc[dcc_cnt].push_back(y);
} while (y != j);
dcc[dcc_cnt].push_back(u);
}
}
else low[u] = min(low[u], dfn[j]);
}
}
二分图
1.二分图不存在奇数环,染色法不存在矛盾
2.匈牙利算法,匹配,最大匹配,匹配点,增广路径
3.最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖
匈牙利算法
bool find(int u) {
for(auto v : eg[u]) {
if(!st[v]) {
st[v] = 1;
int t = match[v];
if(t == 0 || find(t)) {
match[v] = u;
return true;
}
}
}
return false;
}
for(int i = 1; i <= n; i++) {
memset(st, 0, sizeof st);
ans += find(i);
}
最小路径覆盖 DAG
不相交
DAG,有向无环图,用最少的互不相交的路径,将所有点覆盖。
拆点,将原图中1…n,拆成新图1…n,1’…n’,原图i->j的有向边变成新图中i->j’的无向边,新图一定为二分图。
(实际上不需要新建一张图)
最小路径覆盖 = 原图点数 - 新图最大匹配数
可相交
用最少的可相交的路径,将所有点覆盖
对原图G,先求出传递闭包G’,然后对G’转化为不相交问题
code
//floyd求传递闭包
for(int k = 1; k <= n; k ++)
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
d[i][j] |= d[i][k] & d[k][j];
int ans = 0;
//匈牙利算法
for(int i = 1; i <= n; i ++) {
memset(st, 0, sizeof st);
ans += find(i);
}
cout << n - ans << "\n";
拓扑排序
无向图求环
void topo()
{
queue<int> q;
for(int i=1;i<=n;i++)
if(d[i]==1) q.push(i); //不是环内的点,入队
while(!q.empty())
{
int u=q.front();
vis[u]=1;
q.pop();
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(--d[v]==1) q.push(v); //u和v断开,v的度数-1,减一后若度数变为,则可以入队
}
}
}
// 度数为2的点是环内的点
欧拉回路和欧拉路径
无向图
对所有边都连通的无向图
(1)存在欧拉路径的充分必要条件:度数为奇数的点只能有0或2个。
(2)存在欧拉回路的充分必要条件:度数为奇数的点只能有0个。
void dfs(int u) {
for(int i = 1; i <= 500; i ++) {
if(g[u][i]) {
g[u][i] --, g[i][u] --;
dfs(i);
}
}
ans[++ cnt] = u;
}
有向图
对所有边都连通的有向图
(1)存在欧拉路径的充分必要条件:要么所有点的出度均等于入度;要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点)。
(2)存在欧拉回路的充分必要条件:所有点的出度均等于入度。
数据结构
并查集
int get(int x) {
if(fa[x] != x) fa[x] = get(fa[x]);
return fa[x];
}
void merge(int x, int y) {
x = get(x), y = get(y);
if(x == y) return;
fa[x] = y;
val[y] += val[x];
}
堆
左偏树
struct node {
int ls, rs, d;
ll cost, sum, siz;
}t[N];
int id[N], val[N], cost[N];
void pushup(int x) {
t[x].sum = t[t[x].ls].sum + t[t[x].rs].sum + t[x].cost;
t[x].siz = t[t[x].ls].siz + t[t[x].rs].siz + 1;
}
int merge(int x, int y) {
if (!x || !y) return x | y; // 若一个堆为空则返回另一个堆
if (t[x].cost < t[y].cost) swap(x, y); // 取值较大的作为根
t[x].rs = merge(t[x].rs, y); // 递归合并右儿子与另一个堆
if (t[t[x].rs].d > t[t[x].ls].d)
swap(t[x].ls, t[x].rs); // 若不满足左偏性质则交换左右儿子
t[x].d = t[t[x].rs].d + 1; // 更新dist
pushup(x);
return x;
}
void build() {
for(int i = 1; i <= n; i ++) id[i] = i, t[i].siz = 1, t[i].cost = cost[i], t[i].sum = cost[i];
}
树状数组
int lowbit(int x)
{
return x & -x;
}
void update(int x, int c) // 位置x加c
{
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
线段树
struct Node
{
int l, r;
// TODO: 需要维护的信息和懒标记
}tr[N * 4];
void pushup(int u)
{
// tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
// TODO: 利用左右儿子信息维护当前节点的信息
}
void pushdown(int u)
{
// TODO: 将懒标记下传
}
void build(int u, int l, int r)
{
if (l == r) tr[u] = {l, r};
else
{
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u);
}
}
void update(int u, int l, int r, int d)
{
if (tr[u].l >= l && tr[u].r <= r)
{
// TODO: 修改区间
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) update(u << 1, l, r, d);
if (r > mid) update(u << 1 | 1, l, r, d);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)
{
return ; // TODO 需要补充返回值
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
int res = 0;
if (l <= mid ) res = query(u << 1, l, r);
if (r > mid) res += query(u << 1 | 1, l, r);
return res;
}
}
扫描线
void pushup(int u)
{
if (tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];
else if (tr[u].l != tr[u].r)
{
tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;
}
else tr[u].len = 0;
}
void modify(int u, int l, int r, int k)
{
if (tr[u].l >= l && tr[u].r <= r)
{
tr[u].cnt += k;
pushup(u);
}
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r, k);
if (r > mid) modify(u << 1 | 1, l, r, k);
pushup(u);
}
}
int main() {
for (int i = 0, j = 0; i < n; i ++ )
{
double x1, y1, x2, y2;
scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);
seg[j ++ ] = {x1, y1, y2, 1};
seg[j ++ ] = {x2, y1, y2, -1};
ys.push_back(y1), ys.push_back(y2);
}
sort(ys.begin(), ys.end());
ys.erase(unique(ys.begin(), ys.end()), ys.end());
build(1, 0, ys.size() - 2);
sort(seg, seg + n * 2);
double res = 0;
for (int i = 0; i < n * 2; i ++ )
{
if (i > 0) res += tr[1].len * (seg[i].x - seg[i - 1].x);
modify(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].k);
}
}
分块
int id[N], len, tot;
struct node {
int l, r;
ll sum, tag;
}blk[N];
int n, m;
void update(int l, int r, ll val) {
int idx = id[l], idy = id[r];
if(idx == idy) {
for(int i = l; i <= r; i ++)
a[i] += val;
blk[idx].sum += (r - l + 1) * val;
} else {
for(int i = l; i <= blk[idx].r; i ++)
a[i] += val;
blk[idx].sum += (blk[idx].r - l + 1) * val;
for(int i = blk[idy].l; i <= r; i ++)
a[i] += val;
blk[idy].sum += (r - blk[idy].l + 1) * val;
for(int i = idx + 1; i < idy; i ++) {
blk[i].tag += val;
blk[i].sum += val * (blk[i].r - blk[i].l + 1);
}
}
}
ll query(int l, int r) {
int idx = id[l], idy = id[r];
ll ans = 0;
if(idx == idy) {
for(int i = l; i <= r; i ++) {
ans += a[i] + blk[idx].tag;
}
return ans;
} else {
for(int i = l; i <= blk[idx].r; i ++)
ans += a[i] + blk[idx].tag;
for(int i = blk[idy].l; i <= r; i ++)
ans += a[i] + blk[idy].tag;
for(int i = idx + 1; i < idy; i ++)
ans += blk[i].sum;
return ans;
}
}
void build() {
len = sqrt(n);
for(int i = 1; i <= n; i ++) {
id[i] = (i - 1) / len + 1;
blk[id[i]].sum += a[i];
if((i - 1) % len==0) blk[++tot].l = i;
if(i%len == 0) blk[tot].r = i;
}
blk[tot].r = n;
}
莫队
struct mo {
int l, r;
int id;
bool operator < (const mo &o) const
{
if(l / len != o.l / len)
return l < o.l;
if(l / len & 1)
return r < o.r;
else return r > o.r;
}
}q[N];
l = 1, r = 0, len = sqrt(n);
带修莫队
// 增加时间戳
bool operator < (const mo &o) const
{
if(l / len != o.l / len)
return l < o.l;
if(r / len != o.r / len)
return r < o.r;
return t < o.t;
}
// 进行单点修改后,交换原数组与操作子的值
while(now < q[i].t) {
now ++;
if(upd[now].x >= l && upd[now].x <= r) {
cnt[a[upd[now].x]] --;
if(cnt[a[upd[now].x]] == 0) cur --;
if(cnt[upd[now].y] == 0) cur ++;
cnt[upd[now].y] ++;
}
swap(a[upd[now].x], upd[now].y);
}
while(now > q[i].t) {
if(upd[now].x >= l && upd[now].x <= r) {
cnt[a[upd[now].x]] --;
if(cnt[a[upd[now].x]] == 0) cur --;
if(cnt[upd[now].y] == 0) cur ++;
cnt[upd[now].y] ++;
}
swap(a[upd[now].x], upd[now].y);
now --;
}
可持久化tire
int n, m;
int s[N];
int tr[M][2], max_id[M];
int root[N], idx;
void insert(int i, int k, int p, int q) {
if(k < 0) {
max_id[q] = i;
return;
}
int v = s[i] >> k&1;
if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
tr[q][v] = ++ idx;
insert(i, k - 1, tr[p][v], tr[q][v]);
max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}
int query(int root, int C, int L) {
int p = root;
for (int i = 23; i >= 0; i --) {
int v = C >> i & 1;
if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];
else p = tr[p][v];
}
return C ^ s[max_id[p]];
}
void tire_init() {
max_id[0] = -1;
root[0] = ++idx;
insert(0, 23, 0, root[0]);
}
for(int i = 1; i <= n; i ++) {
ll tmp;
cin >> tmp;
s[i] = s[i - 1] ^ tmp;
root[i] = ++ idx;
insert(i, 23, root[i - 1], root[i]);
}
字符串
AC自动机
一
//有多少个不同的模式串在文本串里出现过
namespace AC {
int tr[N][26], tot;
int e[N], fail[N];
void insert(char *s) {
int u = 0;
for (int i = 1; s[i]; i ++) {
if(!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++tot;
u = tr[u][s[i] - 'a'];
}
e[u] ++;
}
queue<int> q;
void build() {
for(int i = 0; i < 26; i++)
if(tr[0][i]) q.push(tr[0][i]);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; i ++) {
if (tr[u][i]) {
fail[tr[u][i]] = tr[fail[u]][i];
q.push(tr[u][i]);
} else tr[u][i] = tr[fail[u]][i];
}
}
}
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a'];
for (int j = u; j && e[j] != -1; j = fail[j]) {
res += e[j], e[j] = -1;
}
}
return res;
}
}
二
//AC自动机
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
char s[N];
char a[N];
namespace AC {
int tr[N][26], tot;
int idx[N], fail[N];
int val[N];
int vis[N];
int cnt[N]; // 第i个字符串的出现次数
void insert(char *s, int id) {
int u = 0;
for (int i = 1; s[i]; i ++) {
if(!tr[u][s[i] - 'a']) tr[u][s[i] - 'a'] = ++tot;
u = tr[u][s[i] - 'a'];
}
vis[id] = u;
}
void init() {
memset(fail, 0, sizeof fail);
memset(tr, 0, sizeof tr);
memset(idx, 0, sizeof idx);
memset(val, 0, sizeof val);
memset(cnt, 0, sizeof cnt);
tot = 0;
}
queue<int> q;
void build() {
for(int i = 0; i < 26; i++)
if(tr[0][i]) q.push(tr[0][i]);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; i ++) {
if (tr[u][i]) {
fail[tr[u][i]] = tr[fail[u]][i];
q.push(tr[u][i]);
} else tr[u][i] = tr[fail[u]][i];
}
}
}
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a'];
for (int j = u; j ; j = fail[j]) res val[j] ++;
}
return res;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n;
cin >> n;
// AC::init();
for(int i = 1; i <= n; i ++) {
cin >> s + 1;
AC::insert(s, i);
}
AC::build();
cin >> a + 1;
int x = AC::query(a);
for(int i = 1; i <= n; i ++) {
cout << AC::val[AC::vis[i]] << "\n";
}
}
三.树上差分优化
int head[N], nxt[N], to[N], cnt;
void add(int u, int v) {
nxt[++cnt] = head[u];
head[u] = cnt;
to[cnt] = v;
}
void dfs(int u) {
int i, v;
for(i = head[u]; i; i = nxt[i]) {
v = to[i];
dfs(v);
val[u] += val[v];
}
}
int query(char *t) {
int u = 0, res = 0;
for (int i = 1; t[i]; i++) {
u = tr[u][t[i] - 'a'];
++val[u];
}
for(int i = 1; i <= tot; i ++) add(fail[i], i);
dfs(0);
return res;
}
后缀自动机
节点含义
每个节点代表的是一个连续长度的字符串的集合,这个集合里面的字符串的长度是连续的,并且每个长度都只会出现一次,字符串集合里面字符串都是长度比它大的字符串的后缀
Fail指针
跳到比当前节点短的最长的相同后缀字符串集
Fail[x]的字符串集长度和x的字符串集长度是连续的
struct Suffix_Automata {
//两倍字符串长度的空间
int maxlen[Maxn], trans[Maxn][26], link[Maxn], Size, Last;
int a[Maxn], b[Maxn], endpos[Maxn];
int dp[Maxn], sum[Maxn];
Suffix_Automata() {Size = Last = 1;}
inline void Extend(int id) {
int cur = (++ Size), p;
maxlen[cur] = maxlen[Last] + 1;
endpos[cur] = 1;
for(p = Last; p && !trans[p][id]; p = link[p]) trans[p][id] = cur;
if (!p) link[cur] = 1;
else {
int q = trans[p][id];
if (maxlen[q] == maxlen[p] + 1) link[cur] = q;
else {
int clone = (++ Size);
maxlen[clone] = maxlen[p] + 1;
for(int i = 0; i < 26; ++i) trans[clone][i] = trans[q][i];
link[clone] = link[q];
for(; p && trans[p][id] == q; p = link[p]) trans[p][id] = clone;
link[cur] = link[q] = clone;
}
}
Last = cur;
}
void getTP(int &Len) { //getendpos 前置
for(int i = 1; i <= Size; i ++) a[maxlen[i]] ++;
for(int i = 1; i <= Len; i ++) a[i] += a[i - 1];
for(int i = 1; i <= Size; i++) b[a[maxlen[i]] --] = i;
}
void getendpos() { //求每类子串的数量
for(int i = Size; i >= 1; i --) {
int e = b[i];
endpos[link[e]] += endpos[e];
}
}
ll getSubNum() { // 求不相同子串数量
ll ans = 0;
for(int i = 2 ;i <= Size; ++i)
ans += maxlen[i] - maxlen[link[i]] ;
return ans;
}
void get_len_max(int Len) { // 求长度为i的出现次数最多的子串
for(int i = 1; i <= Size; i++) dp[maxlen[i]] = max(dp[maxlen[i]], endpos[i]);
for(int i = Len - 1; i >= 1; i --) dp[i] = max(dp[i], dp[i + 1]);
for(int i = 1; i <= Len; i ++) {
cout << dp[i] << "\n";
}
}
void getsum() { //getmink前置
for(int i = Size; i >= 1; i --) {
int &e = b[i];
sum[e] = 1;
for(int j = 0; j < 26; j ++) {
sum[e] += sum[trans[e][j]];
}
}
}
void getmink(int k) { // 求字典序第k小的子串
int now = 1, p;
string s = "";
while(k) {
for (int i = 0; i < 26; i ++) {
if (trans[now][i] && k) {
p = trans[now][i];
if (sum[p] < k) k -= sum[p];
else {
k --; now = p;
s += (char)(i + 'a');
break;
}
}
}
}
cout << s << "\n";
}
void LCS(char s[], int Len) { // 求两个串的最长公共子串
int ans = 0, cnt = 0;
int now = 1;
char base = 'a';
for(int i = 0; i < Len; i++) {
int c = s[i] - base;
if (trans[now][c]) {
cnt ++;
now = trans[now][c];
} else {
while(now&&!trans[now][c]) now = link[now];
if (!now) cnt=0, now=1;
else cnt = maxlen[now]+1, now = trans[now][c];
}
ans = max(ans, cnt);
}
cout << ans << "\n";
}
int ans[Maxn];
void init_ans(){
for(int i=1;i<=Size;i++) ans[i]= maxlen[i];
}
void LCS2(char s[],int Len){ //求多个串的最长公共子串
for(int i=1;i<=Size;i++) dp[i]=0;
int cnt=0;
int now=1;
char base='a';
for(int i=0;i<Len;i++){
int c=s[i]-base;
if(trans[now][c]){
cnt++;
now=trans[now][c];
}
else{
while(now&&!trans[now][c]) now=link[now];
if(!now) cnt=0,now=1;
else cnt=maxlen[now]+1,now=trans[now][c];
}
dp[now]=max(dp[now],cnt);
}
for(int i=Size;i>=1;i--){
int e=b[i];
dp[link[e]]=max(dp[link[e]],min(dp[e],maxlen[link[e]]));
}
for(int i=1;i<=Size;i++) ans[i]=min(ans[i],dp[i]);
}
void get_LCS2_ans(){
int cnt=0;
for(int i=1;i<=Size;i++) cnt=max(cnt,ans[i]);
printf("%d\n",cnt);
}
void get_cntk(int k) { //求出现次数为k的子串种数
ll ans = 0;
for (int i = 1; i <= Size; i++) {
if(endpos[i] == k) ans += maxlen[i] - maxlen[link[i]];
}
cout << ans << "\n";
}
int d[Maxn];
void get_sumk(int l, int r) { //求出现次数 l <= k <= r 的子串种数
for(int i = Size; i > 1; i--) {
int v = b[i];
if(endpos[v] >= l && endpos[v] <= r) d[v] ++;
for(int j = 0; j < 26; j ++) {
if(trans[v][j]) d[v] += d[trans[v][j]];
}
}
ll ans = 0;
for (int i = 0; i < 26; i ++) {
if (trans[1][i]) ans += d[trans[1][i]];
}
cout << ans << "\n";
}
} T;
hash
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
const int base = 131;
using ull = unsigned long long;
char s[N];
ull h1[N], p[N], h2[N], ans = 0;
int n;
ull gh1(int l, int r) {
if(l > r) return 0;
return h1[r] - h1[l - 1] * p[r - l + 1];
}
ull gh2(int l, int r) {
if(l > r) return 0;
return h2[l] - h2[r + 1] * p[r - l + 1];
}
int main() {
scanf("%s", s + 1); p[0] = 1;
n = strlen(s + 1);
for(int i = 1; i <= n; ++i) {
h1[i] = h1[i - 1] * base + s[i];
p[i] = p[i - 1] * base;
}
int f = 1;
for(int i = n; i; i--) h2[i] = h2[i + 1] * base + s[i];
for(int i = 1; i <= n; i ++) {
if(s[i] != s[n - i + 1]) {
f = 0;
for(int j = i; j <= n - i + 1; j ++) {
ull tmp1 = gh2(i, j);
ull tmp2 = gh1(j + 1, n - i + 1);
ull tmp3 = tmp1 * p[n - i + 1 - j] + tmp2;
tmp1 = gh2(j + 1, n - i + 1);
tmp2 = gh1(i, j);
ull tmp4 = tmp1 * p[j - i + 1] + tmp2 ;
if(tmp3 == tmp4) {
cout << i << " " << j << "\n";
return 0;
}
}
for(int j = n - i + 1; j >= i; j --) {
ull tmp2 = gh2(j, n - i + 1);
ull tmp1;
tmp1 = gh1(i, j - 1);
ull tmp3 = tmp1 * p[n - i + 2 - j] + tmp2;
tmp2 = gh2(i, j - 1);
tmp1 = gh1(j, n - i + 1);
ull tmp4 = tmp1 * p[j - i] + tmp2 ;
if(tmp3 == tmp4) {
cout << j << " " << n - i + 1 << "\n";
return 0;
}
}
break;
}
}
if(f) {
cout << 1 << " " << 1 << "\n";
} else cout << -1 << " " << -1 << "\n";
}
多项式
卷积
给定两个多项式 F ( x ) F(x) F(x)和 G ( x ) G(x) G(x),求解 F ( x ) ∗ G ( x ) F(x)*G(x) F(x)∗G(x),时间复杂度为 n l o g ( n ) nlog(n) nlog(n)
FFT
NTT
只能求整数,并且模数必须是 q ∗ 2 k + 1 q*2^{k}+1 q∗2k+1的质数p, ( q , k ) (q,k) (q,k)可以取 ( 7 , 26 ) , ( 199 , 23 ) , ( 17 , 27 ) , ( 479 , 21 ) (7,26),(199,23),(17,27),(479,21) (7,26),(199,23),(17,27),(479,21)。
constexpr ll mod = 998244353, g = 3;
LL power(LL a, LL r) {
LL res = 1;
for (; r; r >>= 1, a = a * a % mod) if (r & 1) res = res * a % mod;
return res;
}
void DFT(LL* a, int n, bool inv = false) {
static vector<int> r;
if ((int)r.size() != n) {
r.resize(n);
for (int i = 0; i < n; i += 1) r[i] = i == 1 ? (n >> 1) : r[i >> 1] >> 1 | r[i & 1];
}
for (int i = 0; i < n; i += 1) if (i < r[i]) std::swap(a[i], a[r[i]]);
for (int m = 1; m < n; m <<= 1) {
LL m2 = m << 1, w0 = power(inv ? power(g, mod - 2) : g, (mod - 1) / m2);
for (int i = 0; i < n; i += m2)
for (int j = 0, w = 1; j < m; j += 1, w = w * w0 % mod) {
LL &x = a[m + i + j], &y = a[i + j], t = w * x % mod;
x = y - t; if (x < 0) x += mod;
y += t; if (y >= mod) y -= mod;
}
}
if (inv) for (int i = 0, inv = power(n, mod - 2); i < n; i += 1) a[i] = a[i] * inv % mod;
}
using POFF = vector<LL>;
POFF operator * (const POFF& f, const POFF& g){
int n = f.size() + g.size() - 1, k = 1;
while (k < n) k <<= 1;
POFF h(k << 1);
copy(f.begin(), f.end(), h.begin());
copy(g.begin(), g.end(), h.begin() + k);
DFT(h.data(), k);
DFT(h.data() + k, k);
for (int i = 0; i < k; i += 1) h[i] = h[i] * h[i + k] % mod;
DFT(h.data(), k, true);
h.resize(n);
return h;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n, m;
cin >> n >> m;
POFF f(n), g(m);
for (LL& x : f) cin >> x;
for (LL& x : g) cin >> x;
POFF h = f * g;
for (LL x : h) cout << x << " ";
return 0;
}
// from https://judge.yosupo.jp/submission/61121 Heltion
Trick
∑ i = 1 n a i b i + k 翻转 a 后得到 ∑ i = 1 n a n − i + 1 b i + k ,因为 n − i + 1 + i + k = n + 1 + k \sum_{i=1}^na_ib_{i+k}\ 翻转a后得到\sum_{i=1}^na_{n-i+1}b_{i+k},因为n-i+1+i+k=n+1+k i=1∑naibi+k 翻转a后得到i=1∑nan−i+1bi+k,因为n−i+1+i+k=n+1+k
所以实际上等价于将数组a翻转后,将a倍长,然后求a和b的卷积,它们卷积的第 n + 1 n+1 n+1项到第 n 2 n^2 n2项就是答案。
组合数学
组合
可重集合的组合公式
有k种元素,每个元素有ai,从中无序地选出r个元素,r <= ai,则有选法:
C
k
−
1
+
r
r
C_{k-1+r}^{r}
Ck−1+rr
排列
可重集合的排列公式
有k个元素,每个元素有ai个,不同排列的个数为
(
∑
i
=
1
k
a
i
)
!
∏
i
=
1
k
(
a
i
!
)
\frac{(\sum_{i=1}^{k}a_i)!}{\prod_{i=1}^k(a_i!)}
∏i=1k(ai!)(∑i=1kai)!
公式
一
∑ x = 0 m i n ( i , j ) C i x C j x = C i + j j \sum_{x=0}^{min(i,j)}C_{i}^{x}C{j}^{x}=C_{i+j}^{j} x=0∑min(i,j)CixCjx=Ci+jj
两个集合中各选出x个数,相当于从长度为i+j的数组中选出i个数
将长度为i+j的数组划分为前后两段,第一段i个数,第二段j个数
从中选取i个数,假设其中有x个数落到了第二段,有i−x个数落到了第一段。我们让第一段剩下x个数变成被选择的数,这样也就是 C i x C j x {}C_i^{x}C_j^{x}{} CixCjx
故容易得到原式子成立。
常见技巧
python
输入与输出
第一行包含一个整数 n,表示待处理的整数个数。
第二行包含空格分隔的 n 个整数,依次表示 a1,a2,⋯,an。
n = int(input())
arr = [int(x) for x in sys.stdin.readline().split()]
map
# 初始化
vis = {}
# 是否在vis内
vis.has_key(key)
# 以列表返回可遍历的(键, 值) 元组数组
vis.items()
# 以列表返回一个字典所有的键
vis.keys()
# 删除字典内所有元素
vis.clear()
# 删除字典给定键key对应的值
vis.pop(key)
trick
区间覆盖
覆盖[1,m],给很多[l,r],必须两个区间有重合时才能合并成大区间。举例,要覆盖[1,5],现在有[1,3],[4,5],[3,5]必须选择[1,3],[3,5] 而不是[1,3], [4,5] ,因为后者两个区间没有重合部分。如何维护这种情形下,区间[1,m]是否全部被覆盖?对每个[l,r]使r–即可,只需要用线段树维护区间[1,m-1]的最小值是否大于等于1,添加一个区间就是让区间[l,r-1]加1。
交换变翻转
对于一个01串,我们可以将交换两个字符,变成翻转两个字符各一次,保证翻转结束,1的数量不变,就可以等价成交换次数为翻转次数的一半。
绝对值
|a-b| = max(a-b, b-a)