0%

很妙的一题

题意

给出n个节点m条边的无向连通图和参数k,求:

  • 严格k2大小的点独立集
  • 或大小k的简单环

3kn105,n1m2105

思路

解法1

​ 我们求出一个原图的DFS树,当某个节点u存在反向边时,找出终点v深度最深的反向边.容易看出这个反向边在一个简单环中,且这个简单环不会被其他边分割.如果这个环的大小k,直接输出即可,否则我们取不相邻的k2个点,它们形成一个点独立集.

​ 如果原图无环(或者说是一棵树),那么我们很容易就能求出大小k2的点独立集.

解法2

​ 我们求出任意一个环,然后枚举所有的边1...m.如果某条边把这个环分割开(即起点终点都在环上),那么保留这个环的任意一半,继续该过程.最后我们会得到一个不被其他边分割的简单环.之后的处理同解法1.

​ 如果原图是一棵树,同解法1.

​ (比赛时我用的就是这种做法,但是写T了=-=个人觉得这是最难想也最难写的做法,鬼知道我当时在干什么)

解法3

​ (来自Um_nik的代码)https://codeforces.com/contest/1364/submission/83636767

​ %%%%%

​ 根据原题,无论我们求点独立集还是简单环,都需要最多k个点,那么我们简单粗暴地把所有编号$ > k,().,,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
//D
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4e5+10;
const int maxm = 4e5+10;
struct Edge{
int from,to,next,cnt;
Edge(int from = 0,int to = 0,int next = 0):from(from),to(to),next(next){}
}edge[maxn];
int head[maxn],cnt,d,dep[maxn],p[maxn];
bool vis[maxn],col[maxn];
void addedge(int f,int t){
edge[cnt] = Edge(f,t,head[f]);
head[f] = cnt++;
}
int n,m,k;
void DFS(int u){
if(vis[u])return;
vis[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(vis[v] and dep[v] > dep[u]){
vector<int>cir = {};
int cur = v;
while(cur != u){
cir.push_back(cur);
cur = p[cur];
}
cir.push_back(u);
int s = cir.size();
cout << 2 << endl << s << endl;
for(int yay:cir)cout << yay << " ";
exit(0);
}
else if(!vis[v]){
p[v] = u;
dep[v] = dep[u]+1;
col[v] = !col[u];
DFS(v);
}
}
}
int main(){
ios::sync_with_stdio(0);
memset(head,-1,sizeof(head));
cin >> n >> m >> k;
d = ceil((double)k/2);
for(int i = 1;i <= m;i++){
int f1,t1;
cin >> f1>> t1;
if(f1 > k or t1 > k)continue;
addedge(f1,t1);
addedge(t1,f1);
}
int cnt1 = 0;
for(int i = 1;i <= n;i++)DFS(i);
vector<int>ans = {};
for(int i = 1;i <= n;i++)if(col[i])cnt1++;
int qwq = 0;
if(cnt1 >= d)qwq = 1;
for(int i = 1;i <= n;i++)if(col[i] == qwq){
if((int)ans.size() == d)break;
ans.push_back(i);
}
cout << 1 << endl;
for(int x:ans)cout << x << " ";
return 0;
}

题面:

https://vjudge.net/problem/UVA-11107

思路

​ 这里给出一种字符串Hash+二分的做法.

​ 对每个字符串分别进行Hash,再二分最长子串的长度L.在判断L是否合法时,只需求出每个字符串中长度为L不重复子串,再统计全部字符串中所有子串的个数.如果有子串出现次数大于等于n2+1,那么L合法.

​ 实现上,使用map进行统计(cnt)和判重(vis).对每个字符串s,计算以[1...|s|L+1]为起点,长度为L的子串的hash值hi,去重后添加到cnt里.在添加过程中如果有cnt[hi]n2+1那么L合法,同时将该段子串添加到答案中.输出时对答案去重输出即可,同时注意UVA的毒瘤格式要求=-=

​ 这个做法时间效率很低(在不作优化的情况下跑了3.72s),但是比较好想和好写

代码

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
//UVA11107
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e3+10,maxs = 105;
const ull x = 13331;
int n;
ull xp[maxn],H[maxn][maxs];
string raw[maxs];
vector<string>ans;
bool check(int L){
bool ok = 0;
map<ull,int>cnt;
for(int k = 1;k <= n;k++){
map<ull,bool>vis;
for(int i = 1;i+L-1 <= raw[k].size();i++){
ull u = H[i+L-1][k] - H[i-1][k]*xp[L];
if(!vis[u]){
vis[u] = 1;
cnt[u]++;
if(cnt[u] >= n/2+1)ok = 1;
}
}
}
return ok;
}
void solve(int L){
map<ull,int>cnt;
for(int k = 1;k <= n;k++){
map<ull,bool>vis;
for(int i = 1;i+L-1 <= raw[k].size();i++){
ull u = H[i+L-1][k] - H[i-1][k]*xp[L];
if(!vis[u]){
vis[u] = 1;
cnt[u]++;
if(cnt[u] >= n/2+1){
string yay = "";
for(int j = i;j <= i+L-1;j++)yay += raw[k][j-1];
ans.push_back(yay);
}
}
}
}
return;
}
int main(){
xp[0] = 1;
for(int i = 1;i < maxn;i++)xp[i] = xp[i-1]*x;
bool wtf = 0;
while(scanf("%d",&n) == 1 and n){
if(wtf)cout << endl;
wtf = 1;
ans.clear();
for(int i = 1;i <= n;i++){
string u;
cin >> u;
raw[i] = u;
for(int j = 1;j <= u.size();j++)H[j][i] = H[j-1][i]*x + u[j-1] - 'a';
}
int L = 1,R = maxn,M,len = 0;
while(L <= R){
M = (L+R)/2;
if(check(M)){
len = M;
L = M+1;
}
else R = M-1;
}
solve(len);
sort(ans.begin(),ans.end());
if(ans.size() and len){
for(int i = 0;i < ans.size();i++)if(!i or ans[i] != ans[i-1])cout << ans[i] << endl;
}
else cout << "?\n";
}
return 0;
}

写这个不是因为这题多难或者有意思,纯粹是纪念一下自己有多傻逼=-=

题意

给出A,B,C,D求满足AxByCzD,的x,y,z能组成的三角形个数

1ABCD5e5

思路

很显然只要满足z<x+yCzD就行=-=

那么枚举x,把[x+B,x+C]加1(用前缀和维护一下就行),再枚举z统计满足z<x+y的个数就行=-=

复杂度O(C)

就这玩意都做不出来,我是傻逼

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//C 
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6+10;
typedef long long ll;
int pre[maxn];
int main(){
ll a,b,c,d,ans = 0;
cin >> a >> b >> c >> d;
for(int i = a;i <= b;i++)pre[i+b]++,pre[i+c+1]--;
for(int i = 1;i < maxn;i++)pre[i] += pre[i-1];
for(int i = 0;i < maxn;i++)ans += pre[i]*max(0ll,min(d,(ll)(i-1)) - c + 1);
cout << ans;
return 0;
}

一道有意思的毒瘤

题意

​ 给出数列{a1,a2,...,an},其中每个ai最多只有7个因子.求乘积为完全平方数的最短子序列

1n1e5,1ai1e6

思路

​ 首先条件里"最多只有7个因子"是很有意思的一个提示,它实际上是指每个数最多只有2个素因子

证明:

​ 我们考虑因子个数定理:

​ 以d(N)表示N的因子个数,且N=Σi=1npiki

​ 则有d(N)=i=1n(ki+1)

​ 容易看出当N有3个素因子时,d(N)8.故N最多只有两个素因子.

​ 如此可以分解每个aiai=pk1qk2的形式.因为我们需要求完全平方数,k1,k2可以直接模2.之后有如下三种情况:

  1. ai=1
  2. ai=1pi
  3. ai=piqi 我们要做的就是保证子序列里每个pi,qi的指数为偶数,同时使这个序列最短.

​ 如何高效求解这个问题?上面的因子分解得到了p,q两个因子,让人联想到图的连边.不妨如此建模:将因子作为点,向ai的两个因子pi,qi连一条无向无权边,则问题转化为求这个图里的最小环.(因为一个简单环里每个点的度数都为2,满足每个因子的指数都为偶数这一要求).

​ 对于无向无权图图求最小环我们可以简单地对每个点做BFS.对于一条未访问的边u,v ,如果v已访问过,那么该图存在一个长度为dis[u]+dis[v]+1的环(请自己画图验证).但是这样复杂度为O(n2)

​ 考虑这个图的特殊性质:任意正整数N最多只有一个大于N的素因子,换句话说这个图里每条边都至少有一个点标号小于max{ai}.只需枚举这些点即可.

​ 总复杂度:O(maxain)

代码

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
//CF1325E
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6+10;
const int maxm = 1e7+10;
struct Edge{
int f,t,next;
Edge(int f = 0,int t = 0,int next = 0):f(f),t(t),next(next){}
}edge[maxn];
int cnt,head[maxn],dis[maxn],ans = 1e9;
void addedge(int f,int t){
edge[cnt] = Edge(f,t,head[f]);
head[f] = cnt++;
}
void add(int f,int t){
addedge(f,t);
addedge(t,f);
}
bool visp[maxn],vis[maxn];
vector<int>P,d[maxn];
int n,raw[maxn];
void init(){
visp[1] = 1;
for(int i = 2;i < maxn;i++)if(!visp[i]){
for(int j = i*2;j < maxn;j+=i)visp[j] = 1;
}
for(int i = 2;i < maxn;i++)if(!visp[i]){
P.push_back(i);
}
cin >> n;
for(int i = 1;i <= n;i++){
cin >> raw[i];
int cur = raw[i];
if(!visp[cur]){
d[i].push_back(cur);
continue;
}
for(int j = 2;j*j <= raw[i];j++){
int cnt2 = 0;
while(cur%j == 0){
cur /= j;cnt2++;
}
if(cnt2%2)d[i].push_back(j);
}
if(cur > 1)d[i].push_back(cur);
}
for(int i = 1;i <= n;i++){
if(!d[i].size()){
cout << 1;
exit(0);
}
else if(d[i].size() == 1){
add(1,d[i][0]);
}
else add(d[i][0],d[i][1]);
}
}
void BFS(int s){
memset(dis,-1,sizeof(dis));
dis[s] = 0;
queue<pair<int,int> >q;
q.push(make_pair(s,0));
while(!q.empty()){
auto now = q.front();
q.pop();
int u = now.first,fa = now.second;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(v == fa)continue;
if(dis[v] == -1){
dis[v] = dis[u]+1;
q.push(make_pair(v,u));
}
else{
ans = min(ans,dis[v]+dis[u]+1);
}
}
}
}
int main(){
memset(head,-1,sizeof(head));
init();
for(int i = 1;i <= 1010;i++)BFS(i);
if(ans == 1e9)ans = -1;
cout << ans;
return 0;
}

22n1+22n22n

这个菜鸡现在还什么都不会>_>慢慢更新吧

前置知识

整除分块

f(x)=kx

f(x)分布呈块状,对于任意一个i,有最大的j=kki,使得f(i)==f(i+1)==...=f(j)

对于类似i=1nki的式子,可分块在O(n)时间内计算完毕

代码:

1
2
3
4
5
for(int l = 1;l <= n;l = r+1){
if(k/l)r = min(n,k/(k/l));
else r = n;
ans += (r-l+1)*(k/l);
}

狄利克雷(Dirichlet)卷积

定义

f,g为两个数论函数,其狄利克雷卷积fg为: fg(n)=d|nf(d)g(nd)

常用式子

$$ ε=μ1ε(n)=dnμ(d)d=11d(n)=dn1σ=id1σ(n)=dndφ=μidφ(n)=dndμ(nd)

$$

常用结论

d|nnφ(d)=n

[gcd(i,j)==1]=ϵ(gcd(i,j)=d|gcd(i,j)μ(d)

套路题

i=1ngcd(i,n)

i=1ngcd(i,n)=d|ndi=1n[gcd(i,n)==d]=d|ndi=1nd[gcd(i,n)==1]=d|ndφ(nd)

i=1nj=1ngcd(i,j)

i=1nj=1ngcd(i,j)=d=1ndi=1nj=1n[gcd(i,j)==d]=d=1ndi=1ndj=1nd[gcd(i,j)==1]=d=1nd(2i=1ndφ(i)1)

关于最后一步,我们设f(x)=i=1nj=1n[gcd(i,j)==1]

手画一下,容易看出f(x)=2i=1nφ(i)1

另一种做法:

我们知道φ有这么一个性质,d|nnφ(d)=n

也就是d|gcd(i,j)nφ(d)=gcd(i,j)

那么: i=1nj=1ngcd(i,j)=i=1nj=1nd|gcd(i,j)φ(d)=d=1nφ(d)i=1n[d|i]j=1n[d|j]=d=1nφ(d)ndmd 少处理个前缀和=-=而且我感觉这个做法更好理解

例题:洛谷P2398

i=1nj=i+1ngcd(i,j)

和上面那个基本一样 i=1nj=i+1ngcd(i,j)=d=1ndi=1nj=i+1n[gcd(i,j)==d]=d=1ndi=1ndj=i+1nd[gcd(i,j)==1]=d=1nd(i=1ndφ(i)1) (最后一步的化简和上一题大同小异,手画一下就有了)

i=1ni=1m[gcd(i,j)==1]

前置知识: ϵ(n)=d|nμ(d)={1n=10otherwise

开始愉快地推式子: i=1ni=1m[gcd(i,j)==1]=i=1ni=1mϵ(gcd(i,j))=i=1ni=1md|gcd(i,j)μ(d)=d=1min(n,m)μ(d)i=1n[d|i]j=1m[d|j]=d=1min(n,m)μ(d)ndmd 同理,我们有: i=1nj=1nk=1n[gcd(i,j,k)==1]=d=1nμ(d)(nd)3

题意

给定序列w1,w2,...,wnq组询问,对于每组询问,求img1

当然,需要对这个值膜m

(hexo渲染多重幂有问题,只好放原题图片了)

1n1e5, 1m1e9, 1q1e5

思路

考虑扩展欧拉定理:

ab={abb<φ(p)abmodφ(p)+φ(p)bφ(p)

由于p2φ(p)为偶数,故p=φ(p)的下降速度是log级别的,换句话说经过最多log(p)次迭代之后p便会变为1.由于xmod1==0,我们在l==r或者p==1时便可跳出递归,单组询问的复杂度为O(logp),可以接受

在具体实现时,应预处理出p,φ(p),φ(φ(p)),...,1来减少时间开销,同时修改快速幂来适应扩欧定理(upd函数),细节见代码

代码

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
//CF906D 
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
ll raw[maxn],n,mod;
map<ll,ll>vis;
ll phi(ll x){
int m = (ll)sqrt(x+0.5);
ll tans = x;
for(int i = 2;i <= m;i++)if(!(x%i)){
tans = tans/i*(i-1);
while(!(x%i))x /= i;
}
if(x > 1)tans = tans / x * (x-1);
return tans;
}
inline ll upd(ll x,ll p){
return x<p?x:x%p+p;
}
ll Pow(ll x,ll d,ll p){
ll tans = 1;
if(d == 0)return 1;
ll a = Pow(x,d/2,p);
tans = upd(a*a,p);
if(d%2)tans = upd(tans*x,p);
return upd(tans,p);
}
ll cal(ll l,ll r,ll p){
if(l == r or p == 1)return upd(raw[l],p);
else return Pow(raw[l],cal(l+1,r,vis[p]),p);
}
int main(){
cin >> n >> mod;
ll qwq = mod;
while(qwq != 1){
vis[qwq] = phi(qwq);
qwq = vis[qwq];
}
vis[1] = 1;
for(int i = 1;i <= n;i++)cin >> raw[i];
int q;
cin >> q;
while(q--){
int l,r;
cin >> l >> r;
cout << cal(l,r,mod)%mod << endl;
}
return 0;
}

f(n)=O(g(n))f(n)cfg(n)1f(n)1cfg(n)1g(n)cf1f(n)n>=nf1g(n)=O(1f(n))

题目大意

​ 给定集合{a1,a2,...,an},求gcd({lcm({ai,aj})|i<j})

2n1e5 , 1ai2e5

思路

​ 考虑唯一分解定理:

a=1npiki

b=1npigi

​ 对于 gcdlcm ,我们有:

gcd(a,b)=1npimin(ki,gi)

lcm(a,b)=1npimax(ki,gi)

​ 观察易得,对于答案中的每个质因子pi,它的指数ki{a1,...,an}pi 第二小 的指数k

​ 我们预处理出i的所有质因数p[i],将ai分解,用d[i][j]表示aj的质因子i的指数,排序后扫一遍d[i]即可得到答案

​ 具体来说 ans={id[i][0]d[i].size()==n1id[i][1]d[i].size()n11otherwise

代码

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
//C
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e6+10;
int raw[maxn];
int vis[maxn];
ll Pow(int x,int d){
ll tans = 1;
if(d == 0)return 1;
ll a = Pow(x,d/2);
tans = a*a;
if(d%2)tans = tans*x;
return tans;
}
bool n_prime[maxn];
vector<int>p[maxn],d[maxn];
void prime(){
for(int i = 2;i <maxn;i++){
if(!n_prime[i]){
p[i].push_back(i);
for(int j = i*2;j < maxn;j += i){
n_prime[j] = 1;
p[j].push_back(i);
}
}
}
n_prime[1] = 1;
}
int main(){
prime();
int n;
cin >> n;
for(int i = 1;i <= n;i++)cin >> raw[i];
for(int i = 1;i <= n;i++){
int u = raw[i];
for(int x:p[u]){
int yay = 0;
while(u%x == 0){
yay++;
u /= x;
}
d[x].push_back(yay);
}
}
ll ans = 1;
for(int i = 2;i < maxn;i++){
sort(d[i].begin(),d[i].end());
if(d[i].size() == n-1){
ans *= Pow(i,d[i][0]);
}
else if(d[i].size() > n-1)ans *= Pow(i,d[i][1]);
}
cout << ans;
return 0;
}

  • 本篇仅记录一些杂乱的知识点留作日后整理,更多是作Markdown和LATEX的练习用

[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
typedef long long ll;
typedef long double ld;
const double PI = 3.14159265358979;
const int maxn = 1e5+10;
struct Point{
ld x,y;
Point(ld x = 0,ld y = 0):x(x),y(y){}
Point operator + (Point B){return Point(x+B.x,y+B.y);}
Point operator - (Point B){return Point(x-B.x,y-B.y);}
Point operator * (ld w){return Point(x*w,y*w);}
Point operator / (ld w){return Point(x/w,y/w);}
Point Rotate_90_clock(){return Point(y,-x);}//顺时针旋转90°
ld len2(){return x*x + y*y;}//长度的平方
ld len(){return(sqrt(x*x + y*y));}
bool operator < (const Point & A) const{
return (x == A.x?y < A.y:x < A.x);
}
}point[maxn];
struct Circle{
Point O;
ld R;
Circle(Point O = Point(0,0),ld R = 0):O(O),R(R){}
ld R2(){return R*R;}
};
typedef Point Vector;
ld Dot(Vector A,Vector B){
return A.x*B.x + A.y*B.y;
}//点积
ld Cross(Vector A,Vector B){
return A.x*B.y - A.y*B.x;
}//二维叉积
Point Line_inter(Point P0,Point V0,Point P1,Point V1){
ld t = Cross(P1 - P0,V1)/Cross(V0,V1);
return P0 + V0 * t;
}//返回两向量交点.P和V分别为向量起点终点*
Circle p2circle(Point A,Point B){
Circle tans = Circle((A+B)/2,0);
tans.R = (tans.O - A).len();
return tans;
}//两点的外接圆
Circle p2circle(Point A,Point B,Point C){
Circle tans = Circle(Line_inter((A+B)/2,(B - A).Rotate_90_clock(),(A+C)/2,(C - A).Rotate_90_clock()),0);
tans.R = (tans.O - A).len();
return tans;
}//三点共圆
typedef Point Vector;
inline double to_rad(double deg){
return deg*(PI/180);
}
Vector Rotate(Vector A,double rad){//逆时针
return Vector(A.x*cos(rad) - A.y*sin(rad),A.x*sin(rad) + A.y*cos(rad));
}

点到直线及线段距离

1
2
3
4
5
6
7
8
9
10
11
ld LinePointDist(Point p,Point a,Point b){//点到直线
Vector v1 = b-a,v2 = p-a;
return fabs(Cross(v1,v2) / len(v1));
}
ld SegPointDist(Point p,Point a,Point b){//点到线段
if(a == b)return len(p - a);
Vector v1 = b-a,v2 = p-a,v3 = p-b;
if(Dot(v1,v2) < eps)return len(v2);
if(Dot(v1,v3) > eps)return len(v3);
return LinePointDist(p,a,b);
}

按角度旋转点

设点A(x,y)绕点R(xr,yr)逆时针旋转θ弧度后得到点A(x,y), 则有: x=(xxr)cos(θ)(yyr)sin(θ)+xry=(xxr)sin(θ)+(yyr)cos(θ)+yr

1
2
3
4
5
void rotate(ld & x,ld & y,ld theta,ld rx,ld ry){
ld nx = (x - rx)*cos(theta) - (y - ry)*sin(theta) + rx;
ld ny = (x - rx)*sin(theta) + (y - ry)*cos(theta) + ry;
x = nx,y = ny;
}

凸包

输入顶点, 返回凸包. raw为原顶点数组,hull为凸包顶点数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vector<Point> ConvexHull(vector<Point>raw){
if(raw.size() == 1)return raw;
sort(raw.begin(),raw.end());
int n = raw.size(),idx = 0;
vector<Point>hull(2*n+10);
for(int i = 0;i < n;i++){
while(idx > 1 and Cross(hull[idx-1] - hull[idx-2],raw[i] - hull[idx-1]) <= 0)idx--;
hull[idx++] = raw[i];
}
int k = idx;
for(int i = n-2;i >= 0;i--){
while(idx > k and Cross(hull[idx-1] - hull[idx-2],raw[i] - hull[idx-1]) <= 0)idx--;
hull[idx++] = raw[i];
}
hull.resize(idx-1);
return hull;
}

闵可夫斯基和

点集A,B的闵可夫斯基和为C={a+b|aA,bB}

img

复杂度O(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
vector<Point> minkowski(vector<Point> & C1,vector<Point> & C2){
int n = (int)C1.size(),m = (int)C2.size();
vector<Point>s1(n),s2(m),A(n+m+10);
for(int i = 0;i < n-1;i++)s1[i] = C1[i+1] - C1[i];
s1[n-1] = C1[0] - C1[n-1];
for(int i = 0;i < m-1;i++)s2[i] = C2[i+1] - C2[i];
s2[m-1] = C2[0] - C2[m-1];
int tot = 0;
A[tot] = C1[0] + C2[0];
int p1 = 0,p2 = 0;
while(p1 < n and p2 < m){
tot++;
A[tot] = A[tot-1] + (Cross(s1[p1],s2[p2]) >= eps?s1[p1++]:s2[p2++]);
}
while(p1 < n){
++tot;
A[tot] = A[tot-1] + s1[p1++];
}
while(p2 <= m){
++tot;
A[tot] = A[tot-1] + s2[p2++];
}
A.resize(tot+1);
return A;
}

多边形面积

_Point为多边形点集,n为顶点数

1
2
3
4
5
6
7
double PolyArea(Point * _Point,int n){
double area = 0;
for(int i = 2;i < n;i++){
area += Cross(_Point[i] - _Point[1],_Point[i+1] - _Point[1]);
}
return area/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
58
59
60
61
62
63
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
const int maxn = 1e5+10;
struct Point{
ld x,y;
Point(ld x = 0,ld y = 0):x(x),y(y){}
Point operator + (Point B){return Point(x+B.x,y+B.y);}
Point operator - (Point B){return Point(x-B.x,y-B.y);}
Point operator * (ld w){return Point(x*w,y*w);}
Point operator / (ld w){return Point(x/w,y/w);}
Point Rotate_90_clock(){return Point(y,-x);}//顺时针旋转90°
ld len2(){return x*x + y*y;}//长度的平方
ld len(){return(sqrt(x*x + y*y));}
}point[maxn];
struct Circle{
Point O;
ld R;
Circle(Point O = Point(0,0),ld R = 0):O(O),R(R){}
ld R2(){return R*R;}
};
typedef Point Vector;
ld Dot(Vector A,Vector B){
return A.x*B.x + A.y*B.y;
}//点积
ld Cross(Vector A,Vector B){
return A.x*B.y - A.y*B.x;
}//二维叉积
Point Line_inter(Point P0,Point V0,Point P1,Point V1){
ld t = Cross(P1 - P0,V1)/Cross(V0,V1);
return P0 + V0 * t;
}//返回两向量交点.P和V分别为向量起点终点*
Circle p2circle(Point A,Point B){
Circle tans = Circle((A+B)/2,0);
tans.R = (tans.O - A).len();
return tans;
}//两点的外接圆
Circle p2circle(Point A,Point B,Point C){
Circle tans = Circle(Line_inter((A+B)/2,(B - A).Rotate_90_clock(),(A+C)/2,(C - A).Rotate_90_clock()),0);
tans.R = (tans.O - A).len();
return tans;
}//三点共圆
Circle Min_Cir_Cover(Point * point,int n){
random_shuffle(point+1,point+n+1);
Circle res;
for(int i = 1;i <= n;i++){
if((point[i] - res.O).len2() > res.R2()){
res = Circle(point[i],0);
for(int j = 1;j < i;j++){
if((point[j] - res.O).len2() > res.R2()){
res = p2circle(point[i],point[j]);
for(int k = 1;k < j;k++){
if((point[k] - res.O).len2() > res.R2()){
res = p2circle(point[i],point[j],point[k]);
}
}
}
}
}
}
return res;
}//最小圆覆盖,输入点集数组和点的数量,返回一个圆

判断点在凸包内部及边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bool Left(Point P,Point A,Point B) {
return Cross(B - A,P - A) > 0;
}
bool Right(Point P,Point A,Point B) {
return Cross(P - A,B - A) > 0;
}
bool Point_in_Convex(Point P,vector<Point> & C){
int n = C.size();
if (Left(P,C[0],C[n - 1]) or Right(P,C[0],C[1]))return 0;
int l = 1, r = n - 1;
while (l < r - 1) {
int m = (l + r) / 2;
if (Left(P, C[0], C[m])) {
l = m;
}
else {
r = m;
}
}
if (!Right(P, C[l], C[r]))return 1;
return 0;
}

三维

起手式:

鸽子biss

叉乘写为矩阵形式

a×b=Ab=(0z1y1z10x1y1x10)(x2y2z2)

A称为dual martix

最小球覆盖

来自上交红书《ACM国际大学生程序设计竞赛-算法与实现》

下标从0开始

复杂度: O(n)

输入:

npoint 全局变量, 点的个数

pt 全局变量, 点的坐标

调用 smallest_ball()

输出:

res 全局变量, 球心坐标

radius 全局变量, 球的半径

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
//
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
const int maxn = 2e5+10;
double eps = 1e-9;
struct Tpoint{
double x,y,z;
Tpoint(double x = 0,double y = 0,double z = 0):x(x),y(y),z(z){}
};
int npoint,nouter;
Tpoint pt[maxn],outer[4],res;
double radius,tmp;
inline double dist(Tpoint p1,Tpoint p2){
double dx = p1.x - p2.x,dy = p1.y - p2.y,dz = p1.z - p2.z;
return (dx*dx + dy*dy + dz*dz);
}
inline double dot(Tpoint p1,Tpoint p2){
return p1.x*p2.x + p1.y*p2.y + p1.z*p2.z;
}
void ball(){
Tpoint q[3];double m[3][3], sol[3],L[3],det;
res.x = res.y = res.z = radius = 0;
switch(nouter){
case 1: res = outer[0];break;
case 2:
res.x = (outer[0].x+outer[1].x)/2;
res.y = (outer[0].y+outer[1].y)/2;
res.z = (outer[0].z+outer[1].z)/2;
radius = dist(res,outer[0]);
break;
case 3:
for(int i = 0;i < 2;i++){
q[i].x = outer[i+1].x - outer[0].x;
q[i].y = outer[i+1].y - outer[0].y;
q[i].z = outer[i+1].z - outer[0].z;
}
for(int i = 0;i < 2;i++)
for(int j = 0;j < 2;j++)m[i][j] = dot(q[i],q[j])*2;
for(int i = 0;i < 2;i++)sol[i] = dot(q[i],q[i]);
if(fabs(det = m[0][0]*m[1][1] - m[0][1]*m[1][0])<eps)return;
L[0] = (sol[0]*m[1][1] - sol[1]*m[0][1])/det;
L[1] = (sol[1]*m[0][0] - sol[0]*m[1][0])/det;
res.x = outer[0].x + q[0].x*L[0] + q[1].x*L[1];
res.y = outer[0].y + q[0].y*L[0] + q[1].y*L[1];
res.z = outer[0].z + q[0].z*L[0] + q[1].z*L[1];
radius = dist(res,outer[0]);
break;
case 4:
for(int i = 0;i < 3;i++){
q[i].x = outer[i+1].x - outer[0].x;
q[i].y = outer[i+1].y - outer[0].y;
q[i].z = outer[i+1].z - outer[0].z;
sol[i] = dot(q[i],q[i]);
}
for(int i = 0;i < 3;i++)
for(int j = 0;j < 3;j++)m[i][j] = dot(q[i],q[j])*2;
det = m[0][0] * m[1][1] * m[2][2]
+ m[0][1] * m[1][2] * m[2][0]
+ m[0][2] * m[2][1] * m[1][0]
- m[0][2] * m[1][1] * m[2][0]
- m[0][1] * m[1][0] * m[2][2]
- m[0][0] * m[1][2] * m[2][1];
if(fabs(det) < eps)return;
for(int j = 0;j < 3;j++){
for(int i = 0;i < 3;i++)m[i][j] = sol[i];
L[j] = (m[0][0] * m[1][1] * m[2][2]
+ m[0][1] * m[1][2] * m[2][0]
+ m[0][2] * m[2][1] * m[1][0]
- m[0][2] * m[1][1] * m[2][0]
- m[0][1] * m[1][0] * m[2][2]
- m[0][0] * m[1][2] * m[2][1]
)/det;
for(int i = 0;i < 3;i++)m[i][j] = dot(q[i],q[j])*2;
}
res = outer[0];
for(int i = 0;i < 3;i++){
res.x += q[i].x * L[i];
res.y += q[i].y * L[i];
res.z += q[i].z * L[i];
}
radius = dist(res,outer[0]);
}
}
void minball(int n){
ball();
if(nouter < 4){
for(int i = 0;i < n;i++){
if(dist(res,pt[i]) - radius > eps){
outer[nouter] = pt[i];
++nouter;
minball(i);
--nouter;
if(i > 0){
Tpoint Tt = pt[i];
memmove(&pt[1],&pt[0],sizeof(Tpoint)*i);
pt[0] = Tt;
}
}
}
}
}
double smallest_ball(){
radius = -1;
for(int i = 0;i < npoint;i++){
if(dist(res,pt[i]) - radius > eps){
nouter = 1;
outer[0] = pt[i];
minball(i);
}
}
return sqrt(radius);
}
signed main(){
cin >> npoint;
for(int i = 0;i < npoint;i++){
int x,y,z;
cin >> x >> y >> z;
pt[i] = Tpoint(x,y,z);
}
printf("%.10lf",smallest_ball());
return 0;
}

[TOC]

建图

1
2
3
4
5
6
7
8
9
10
11
12
13
const int maxn = 2e5+10;
const int maxm = maxn*2;
int head[maxn],pa[maxn];
bool vis[maxn];
struct Edge{
int f,t,next;
Edge(int f = 0,int t = 0,int next = 0):f(f),t(t),next(next){}
}edge[maxm];
int tcnt,cnt;
void addedge(int f,int t){
edge[cnt] = Edge(f,t,head[f]);
head[f] = cnt++;
}

最短路

SPFA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void spfa(int s){
queue<int>q;
for(int i = 1;i <= n;i++)dis[i] = INF;
q.push(s);
vis[s] = 1;
dis[s] = 0;
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(dis[v] > dis[u] + edge[i].w){
dis[v] = dis[u] + edge[i].w;
if(!vis[v]){
vis[v] = 1;
q.push(v);
}
}
}
}
}

SPFA优化

来自OI-Wiki

除了队列优化(SPFA)之外,Bellman-Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
  • LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
  • SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。
  • D´Esopo-Pape 算法:将普通队列换成双端队列,如果一个节点之前没有入队,则将其插入队尾,否则插入队首。

Dijkstra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void dijkstra(int s){
typedef pair <int,int> node;
priority_queue<node,vector<node>,greater<node> >q;
for(int i =1;i<=n;i++)dis[i] = INF;
dis[s] = 0;
q.push(make_pair(dis[s],s));
while(!q.empty()){
int u = q.top().second;
q.pop();
if(vis[u])continue;
vis[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(dis[v] > dis[u] + edge[i].w){
dis[v] = dis[u] + edge[i].w;
if(!vis[v]){
q.push(make_pair(dis[v],v));
}
}
}
}
return;
}

网络流

最大流

Edmonds-Karp

(慎用)

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
const int maxn = 10005;
const int maxm = 4e5+10;
const int INF = 1e9;
int head[maxn],parent[maxn],approve[maxn];
struct Edge{
int from,to,next,w,cap,flow;
}edge[maxm];
int n,m,start,to,cnt,ans;//cnt = 0 so we can calculate "backedge" easily
void addedge(int from,int to,int cap,int flow){
edge[cnt].cap = cap;
edge[cnt].flow = flow;
edge[cnt].to = to;
edge[cnt].from = from;
edge[cnt].next = head[from];
head[from] = cnt++;
}
int BFS(int s,int t){
queue<int>q;
memset(approve,0,sizeof(approve));
q.push(s);
approve[s] = INF;
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(!approve[v] and edge[i].cap > edge[i].flow){
parent[v] = i;
approve[v] = min(approve[u],edge[i].cap - edge[i].flow);
q.push(v);
}
}
if(approve[t])break;
}
return approve[t];
}
int EK(int s,int t){
int flow = 0;
while(1){
int delta = BFS(s,t);
if(!delta)break;
for(int u = t;u != s;u = edge[parent[u]].from){
int curp = parent[u];
edge[curp].flow += delta;
edge[curp^1].flow -= delta;
}
flow += delta;
}
return flow;
}

Dinic

记得加上容量为0的反向弧=-=

1
2
addedge(f,t,w,0);
addedge(t,f,0,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
struct Edge{
int t,cap,flow,rev;
bool is_rev;
};
vector<Edge>G[maxn];
int cur[maxn],dis[maxn],vis[maxn];
struct Dinic{
int n;
int flow = 0;
void addedge(int f,int t,int cap,int flow,bool is_rev = 0){
int rev_cnt = G[t].size();
if(is_rev)rev_cnt--;
G[f].push_back({t,cap,flow,rev_cnt,is_rev});
}
void add(int f,int t,int w){
addedge(f,t,w,0,false);
addedge(t,f,0,0,true);
}
void clear(){
for(int i = 0;i <= n;i++){
G[i].clear();
G[i].shrink_to_fit();
cur[i] = vis[i] = dis[i] = 0;
}
flow = 0;
n = 0;
}
void init(int N){
clear();
n = N + 1;
}
bool BFS(int s,int t){
for(int i = 0;i <= n;i++)cur[i] = 0;
for(int i = 0;i <= n;i++)vis[i] = 0;
queue<int>q;
q.push(s);
vis[s] = 1;
dis[s] = 0;
while(q.size()){
int u = q.front();
q.pop();
for(auto it:G[u]){
int v = it.t;
if(!vis[v] and it.cap > it.flow){
vis[v] = 1;
dis[v] = dis[u] + 1;
q.push(v);
}
}
}
return vis[t];
}
int DFS(int u,int T,int appr){
if(u == T or !appr)return appr;
int flow = 0,delta = 0;
for(int & i = cur[u];i < (int)G[u].size();i++){
auto it = G[u][i];
int v = it.t;
// cur[u] = i;
if(dis[u] + 1 == dis[v] and (delta = DFS(v,T,min(appr,it.cap - it.flow))) > 0){
G[u][i].flow += delta;
G[v][it.rev].flow -= delta;
flow += delta;
appr -= delta;
if(!appr)break;
}
}
return flow;
}

int Maxflow(int s,int t){
while(BFS(s,t)){
// cout << "flow: " << flow << endl;
flow += DFS(s,t,INF);
}
return flow;
}
}dic;

最小费用最大流

全局变量flow为最大流,cost为最小费用

记得反向边容量为0费用为负=-=

注意使用到了全局变量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
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
const int maxn = 10005;
const int maxm = 4e5+10;
const int INF = 1e9;
int head[maxn],approve[maxn],dis[maxn],vis[maxn];
pair<int,int> parent[maxn];
struct Edge{
int t,w,cap,flow,cost,rev;
bool is_rev;
};
int n,m,start,to,cnt,ans,flow,cost;
vector<Edge>G[maxn];
void addedge(int f,int t,int cap,int flow,int cost,bool is_rev){
int rev_cnt = G[t].size();
if(is_rev)rev_cnt--;
G[f].push_back({t,0,cap,flow,cost,rev_cnt,is_rev});
}
void add(int f,int t,int cap,int flow,int cost){
addedge(f,t,cap,flow,cost,false);
addedge(t,f,0,0,-cost,true);
}
int spfa(int s,int t){
for(int i = 1;i <= n;i++){
dis[i] = INF;
vis[i] = 0;
approve[i] = 0;
}
queue<int>q;
q.push(s);
approve[s] = INF;
dis[s] = 0;
vis[s] = 1;
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = 0;
for(int i = 0;i < G[u].size();i++){
Edge it = G[u][i];
int v = it.t;
if(dis[v] > dis[u] + it.cost and it.cap > it.flow){
parent[v] = {u,i};
approve[v] = min(approve[u],it.cap - it.flow);
dis[v] = dis[u] + it.cost;
if(!vis[v]){
vis[v] = 1;
q.push(v);
}
}
}
}
return approve[t];
}
int EK(int s,int t){
// int flow = 0;
while(1){
int delta = spfa(s,t);
if(!delta)break;
for(int u = t;u != s;u = parent[u].first){
auto [curpx,curpy] = parent[u];
G[curpx][curpy].flow += delta;
int rev = G[curpx][curpy].rev;
G[u][rev].flow -= delta;
}
flow += delta;
cost += dis[t]*delta;
}
return flow;
}

上下界网络流

以下设源汇点为s1,t1, 超级源汇为S,T, 每条边的上界为u, 下界为l

无源无汇上下界可行流

将原网络拆成两个图: 下界网络(每条边的容量和流量都是li, 不用实际建出) 和差网络(每条边的容量为uili, 流量为0).

pflow[i]为第i个点的净流量(即流入量减流出量). 如果pflow[i]>0, 在差网络上建一条(S,i,pflow[i])的边来补偿i点缺失的流出量. 如果pflow[i]<0, 建一条(i,T,pflow[i])的边. 然后跑从ST的最大流, 如果S的任一出边或者T的任一入边没有满流则不存在可行流. 否则每条边的可行流为flowi+li

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
int n,m;
cin >> n >> m;
int S = n+1,T = n+2;
N = T+1;
memset(head,-1,sizeof(head));
for(int i = 1;i <= m;i++){
int f,t,l,u;
cin >> f >> t >> l >> u;
pflow[f] -= l,pflow[t] += l;
lowedge[cnt] = l;
add(f,t,u-l);
}
for(int i = 1;i <= n;i++){
if(pflow[i] > 0)add(S,i,pflow[i]);
else if(pflow[i] < 0)add(i,T,-pflow[i]);
}
Dinic(S,T);
bool ok = 1;
for(int i = head[S];~i;i = edge[i].next){
if(edge[i].cap != edge[i].flow)ok = 0;
}
cout << (ok?"YES\n":"NO");
if(ok){
for(int i = 0;i < m*2;i += 2)cout << edge[i].flow + lowedge[i] << endl;
}

有源有汇上下界可行流

在无源无汇上下界可行流的基础上, 建一条(ti,si,\infin)的边. 这条边的流量就是可行流流量

有源有汇上下界最大流

在有源有汇上下界可行流的基础上:

先跑一次ST的最大流, 设flow1 = (ti,si,\infin)边的流量并判断是否存在可行流. 如果存在就把这条边删掉, 再在残余网络上跑一次从siti的最大流flow2. 答案即为flow1+flow2

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
int n,m,s1,t1;
cin >> n >> m >> s1 >> t1;
int S = n+1,T = n+2;
N = T+1;
memset(head,-1,sizeof(head));
for(int i = 1;i <= m;i++){
int f,t,l,u;
cin >> f >> t >> l >> u;
pflow[f] -= l,pflow[t] += l;
lowedge[cnt] = l;
add(f,t,u-l);
}
for(int i = 1;i <= n;i++){
if(pflow[i] > 0)add(S,i,pflow[i]);
else if(pflow[i] < 0)add(i,T,-pflow[i]);
}
add(t1,s1,INF);
int flow = 0;
Dinic(S,T);
bool ok = 1;
for(int i = head[S];~i;i = edge[i].next){
if(edge[i].cap != edge[i].flow)ok = 0;
}
for(int i = head[s1];~i;i = edge[i].next){
if(edge[i].t == t1){
flow += abs(edge[i].flow);
edge[i].flow = edge[i^1].flow = edge[i].cap = edge[i^1].cap = 0;
}
}
flow += Dinic(s1,t1);
if(ok){
cout << flow;
}
else cout << "please go home to sleep";

有源有汇上下界最小流

同有源有汇上下界最大流, 只是在第二步时跑tisi的最大流, 答案为flow1flow2

常用模型

DAG最小覆盖问题

给定有向图G=(V,E)n=|V|. 设 PG 的一个简单路(顶点不相交)的集合. 如果 V 中每个定点恰好在P的一条路上, 则称PG的一个路径覆盖. P中路径可以从V的任何一个定点开始, 长度也是任意的, 特别地, 可以为0. G的最小路径覆盖是G所含路径条数最少的路径覆盖.

将原图的点拆为ii+n. 设超级源S, 超级汇T. S向点1...n连边, 点1+n...n+nT连边, 若有<i,j>∈Eij+n连边. 求出ST的最大流f, 则nf为路径条数最少的路径覆盖. 方案可从流量网络中还原.

建图:

1
2
3
4
5
6
7
8
9
10
11
12
int S = 2*n+1,T = 2*n+2;
for(int i = 1;i <= n;i++){
add(S,i,1);
add(i+n,T,1);
}
for(int i = 1;i <= m;i++){
int f,t;
cin >> f >> t;
add(f,t+n,1);
}
N = T;
int ans = Dinic(S,T);

求方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void solution(){
static int nxt[maxn],has[maxn];
for(int i = 1;i <= n;i++)nxt[i] = -1,has[i] = 0;
for(int i = 0;i <= cnt;i+=2){
int u = edge[i].f,v = edge[i].t - n;
if(edge[i].flow == 1 and v > 1 and u <= n){
nxt[u] = v;
has[v] = 1;
}
}
for(int i = 1;i <= n;i++)if(!has[i]){
cout << i << " ";
int u = nxt[i];
while(u != -1){
cout << u << " ";
u = nxt[u];
}
cout << endl;
}
}

二分图匹配

常识性知识

  • 所有的树是二分图

  • 所有的网格图和四边形图是二分图

  • u,v为两个k进制数且u,v只有一位不同且该位差值为1. 若图中的所有边的两个顶点ui,vi均满足该性质, 则该图是二分图(将每一位加起来后的值按奇偶分类)

  • 最大匹配数:最大匹配的匹配边的数目

    最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择

    最大独立数:选取最多的点,使任意所选两点均不相连

    最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。

    定理1:最大匹配数 = 最小点覆盖数(Konig 定理)

    定理2:最大匹配数 = 最大独立数

    定理3:最小路径覆盖数 = 顶点数 - 最大匹配数

二分图最大匹配

网络流做法

建立超级源S和超级汇T, 从S向每个左端点连边, 每个右端点向T连边, 端点之间按给出的图连边(仅从左端点向右端点), 边权均为1.

ST的最大流就是最大匹配

用Dinic实现 复杂度O(nm)

KM

复杂度O(nm)

match[v]v的匹配点.

a为左端点点的个数, b为右端点点的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool KM(int u){
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
vis[v] = 1;
if(!match[v] or KM(match[v])){
match[v] = u;
match[u] = v;
return 1;
}
}
return 0;
}
int match_cnt = 0;
for(int i = 1;i <= a;i++){
for(int j = 0;j <= a+b;j++)vis[j] = 0;
match_cnt += KM(i);
}
cout << match_cnt;

二分图最大带权匹配

复杂度O(n3). 使用addEdge(f,t,w)加边. 注意下标从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
template <typename T>
struct hungarian { // km
int n;
vector<int> matchx; // 左集合对应的匹配点
vector<int> matchy; // 右集合对应的匹配点
vector<int> pre; // 连接右集合的左点
vector<bool> visx; // 拜访数组 左
vector<bool> visy; // 拜访数组 右
vector<T> lx;
vector<T> ly;
vector<vector<T> > g;
vector<T> slack;
T inf;
T res;
queue<int> q;
int org_n;
int org_m;

hungarian(int _n, int _m) {
org_n = _n;
org_m = _m;
n = max(_n, _m);
inf = numeric_limits<T>::max();
res = 0;
g = vector<vector<T> >(n, vector<T>(n));
matchx = vector<int>(n, -1);
matchy = vector<int>(n, -1);
pre = vector<int>(n);
visx = vector<bool>(n);
visy = vector<bool>(n);
lx = vector<T>(n, -inf);
ly = vector<T>(n);
slack = vector<T>(n);
}

void addEdge(int u, int v, int w) {
g[u][v] = max(w, 0); // 负值还不如不匹配 因此设为0不影响
}

bool check(int v) {
visy[v] = true;
if (matchy[v] != -1) {
q.push(matchy[v]);
visx[matchy[v]] = true; // in S
return false;
}
// 找到新的未匹配点 更新匹配点 pre 数组记录着"非匹配边"上与之相连的点
while (v != -1) {
matchy[v] = pre[v];
swap(v, matchx[pre[v]]);
}
return true;
}

void bfs(int i) {
while (!q.empty()) {
q.pop();
}
q.push(i);
visx[i] = true;
while (true) {
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v = 0; v < n; v++) {
if (!visy[v]) {
T delta = lx[u] + ly[v] - g[u][v];
if (slack[v] >= delta) {
pre[v] = u;
if (delta) {
slack[v] = delta;
} else if (check(v)) { // delta=0 代表有机会加入相等子图 找增广路
// 找到就return 重建交错树
return;
}
}
}
}
}
// 没有增广路 修改顶标
T a = inf;
for (int j = 0; j < n; j++) {
if (!visy[j]) {
a = min(a, slack[j]);
}
}
for (int j = 0; j < n; j++) {
if (visx[j]) { // S
lx[j] -= a;
}
if (visy[j]) { // T
ly[j] += a;
} else { // T'
slack[j] -= a;
}
}
for (int j = 0; j < n; j++) {
if (!visy[j] && slack[j] == 0 && check(j)) {
return;
}
}
}
}

void solve() {
// 初始顶标
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
lx[i] = max(lx[i], g[i][j]);
}
}

for (int i = 0; i < n; i++) {
fill(slack.begin(), slack.end(), inf);
fill(visx.begin(), visx.end(), false);
fill(visy.begin(), visy.end(), false);
bfs(i);
}

// custom
for (int i = 0; i < n; i++) {
if (g[i][matchx[i]] > 0) {
res += g[i][matchx[i]];
} else {
matchx[i] = -1;
}
}
cout << res << "\n";
for (int i = 0; i < org_n; i++) {
cout << matchx[i] + 1 << " ";
}
cout << "\n";
}
};

应用

二分图最大点独立集

个数为顶点数-最大匹配数

二分图最小点覆盖(König 定理)

最小点覆盖: 选择最小的点集S 使得任意一条边至少有一个顶点在S里. 二分图的最小点覆盖数 = 最大匹配数.

构造方案: 从左边的未匹配点开始dfs, 从左向右走未匹配边, 从右向左走匹配边(类似KM), 将经过的点全部标记. 左边的未标记点和右边的标记点就是S.

match[v] = u表示u,v是一条匹配边. mvis[]代表该过程标记的点. (在用Dinic求二分图时需要去掉源点汇点S,T)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int match[maxn],mvis[maxn];
void MVC(int u,int S,int T,bool flag){
if(u == S or u == T)return;
mvis[u] = 1;
if(flag){
if(!mvis[match[u]])MVC(match[u],S,T,!flag);
}
else{
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(mvis[v])continue;
MVC(v,S,T,!flag);
}
}
}

mvc即为所求点集:

1
2
3
4
5
6
for(int i = 1;i <= n1;i++)if(!match[i]){
MVC(i,S,T,0);
}
vector<int>mvc;
for(int i = 1;i <= n1;i++)if(!mvis[i])mvc.push_back(i);
for(int i = n1+1;i <= n1+n2;i++)if(mvis[i])mvc.push_back(i);

二分图博弈

考虑如下问题: 有两名玩家AB, 轮流进行游戏且A先手. 玩家每次可从当前状态转移到相邻的状态, 且已经访问过的状态不能再次访问. 状态可分为两个不相交的集合(建成二分图). 不能操作的玩家输.

概念:

  • 完美匹配: 图中所有点均属于最大匹配
  • 必经点: 任意一个最大匹配均包含的点
  • 非必经点: 至少一个最大匹配不包含的点.

结论: 如果状态图最大匹配是完美匹配, 则A必败. 否则, 如果游戏从非必经点开始, A必胜. 其他情况A必败.

简要推导: 如果A从必经点开始, 那么B总存在一种策略让自己走非匹配边, 使得A必须走上匹配边, 最后A将无路可走. 而如果A从非必经点开始, 则A一定会走到一个必经点, 转换为B从必经点开始.

至此问题转化为求二分图中的所有非必经点.

方法一: KM

时间复杂度O(nm), 适用于比较小的图.

全局变量: bool unne[]. unne[i]=1表示i为非必经点.

需要先跑一遍KM, 再对每个点iunness(i). 注意特判度为0的点

1
2
3
4
5
6
7
8
9
bool unne[maxn];
void unness(int u){
if(unne[u])return;
unne[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(match[v])unness(match[v]);
}
}

方法2:网络流

这里用Dinic跑. 建图约定: 和超级源相连的点颜色标为1, 和超级汇相连的点颜色标为0. 从S出发走非满流边能到达的和S相邻的点, 以及从T出发走满流边能到达的和T相邻的点均为非必经点. 复杂度O(nm)

使用时调用cal_unne(S,1)cal_unne(T,0).

1
2
3
4
5
6
7
8
bool unne[maxn];
void cal_unne(int u,int c){
if(col[u] == c)unne[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(edge[i].cap - edge[i].flow == c and !unne[v])cal_unne(v,c);
}
}

方法3: 判断单个点

假设判断点s. 先建出整个图, 但不连接Ss, sT. 跑一遍网络流. 再将Ss,sT连接, 在残量网络里继续跑网络流. 如果流量增加那么s是非必经点.

连通性与环

割点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Tarjan(int u,int fa){
if(dfn[u])return;
dfn[u] = low[u] = dfsclock++;
int child = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(!dfn[v]){
Tarjan(v,fa);
low[u] = min(low[v],low[u]);
if(u == fa)child++;
if(low[v] >= dfn[u] and u != fa)iscut[u] = 1;
}
low[u] = min(low[u],dfn[v]);
}
if(fa == u and child > 1)iscut[u] = 1;
return;
}

SCC/强连通分量

Kosaraju

G为原图,GT为反图,GD为缩点后的DAG

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
vector<int>G[maxn],GT[maxn],GD[maxn],GDR[maxn];
int sccno[maxn],siz[maxn];
bool vis[maxn];
int out[maxn];
int scccnt;
map<pair<int,int>,bool>dup;
vector<int>r_topo;
void add(int f,int t){
G[f].push_back(t);
GT[t].push_back(f);
}
void addDAG(int f,int t){
GD[f].push_back(t);
GDR[t].push_back(f);
out[f]++;
}
void DFS1(int u){
if(vis[u])return;
vis[u] = 1;
for(int v:G[u])DFS1(v);
r_topo.push_back(u);
return;
}
void DFS2(int u){
if(sccno[u])return;
sccno[u] = scccnt;
siz[scccnt]++;
for(int v:GT[u])DFS2(v);
return;
}
void findscc(int n){
for(int i = 1;i <= n;i++)DFS1(i);
for(int i = n-1;i >= 0;i--){
if(!sccno[r_topo[i]]){
scccnt++;
DFS2(r_topo[i]);
}
}
}
void buildmap(int n){
for(int u = 1;u <= n;u++){
for(int v:G[u]){
if(sccno[u] != sccno[v]){
// not nessseary
// if(!dup[{sccno[u],sccno[v]}]){
// dup[{sccno[u],sccno[v]}] = 1;
// }
addDAG(sccno[u],sccno[v]);
}
}
}
}

一种比较好理解的求桥的方法 (注意无法用来求割顶

dp[u]表示跨过u的反向边的数量.如果dp[u]==0<fa,u>为桥(fau的父亲)

调用时需初始化dep[root]=1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Cal_bri(int u,int fa){
dp[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(v == fa)continue;
if(!dep[v]){
dep[v] = dep[u]+1;
Cal_bri(v,u);
dp[u] += dp[v];
}
else{
if(dep[v] > dep[u])dp[u]--;
else if(dep[v] < dep[u]){
dp[u]++;
}
}
}
if(!dp[u] and dep[u] > 1)ok = 0;
}

仙人掌

(接上文)点仙人掌中求环并缩点

backcnt为全局变量,初始化为n+1(点的个数+1)

phash[u]返回环的标号,从n+1开始.若不在环内则phash[u]==u

调用时初始化dep[root]=1,注意缩点后需要重新寻找根节点(缩点代码未给出)

原理参见https://codeforces.com/blog/entry/68138 这个东西应该找简单环都可以用(?

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
void Cal_bri(int u,int fa){
dp[u] = 0;
bool ok = 0;
for(int i = rhead[u];~i;i = redge[i].next){
int v = redge[i].to;
if(v == fa)continue;
if(!dep[v]){
dep[v] = dep[u]+1;
Cal_bri(v,u);
dp[u] += dp[v];
}
else{
if(dep[v] > dep[u])dp[u]--;
else if(dep[v] < dep[u]){
in[v]++;
dp[u]++;
ok = 1;
redge[i].cnt = ++backcnt;
}
}
}
if(ok)phash[u] = backcnt;
else{
phash[u] = u;
for(int i = rhead[u];~i;i = redge[i].next){
int v = redge[i].to;
if(v == fa)continue;
if(phash[v] != v and !in[v]){
phash[u] = phash[v];
}
}
}
}

BFS求最小环

见代码=-=可以画个BFS树或者手模感悟一下

记得得枚举1...n的所有起点,所以复杂度是O(n2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void BFS(int s){
memset(dis,-1,sizeof(dis));
dis[s] = 0;
queue<pair<int,int> >q;
q.push(make_pair(s,0));
while(!q.empty()){
auto now = q.front();
q.pop();
int u = now.first,fa = now.second;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(v == fa)continue;
if(dis[v] == -1){
dis[v] = dis[u]+1;
q.push(make_pair(v,u));
}
else{
ans = min(ans,dis[v]+dis[u]+1);
}
}
}
}

Floyd求最小环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int val[maxn + 1][maxn + 1];  // 原图的邻接矩阵
inline int floyd(const int &n) {
static int dis[maxn + 1][maxn + 1]; // 最短路矩阵
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) dis[i][j] = val[i][j]; // 初始化最短路矩阵
int ans = inf;
for (int k = 1; k <= n; ++k) {
for (int i = 1; i < k; ++i)
for (int j = 1; j < i; ++j)
ans = std::min(ans, dis[i][j] + val[i][k] + val[k][j]); // 更新答案
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] = std::min(
dis[i][j], dis[i][k] + dis[k][j]); // 正常的 floyd 更新最短路矩阵
}
return ans;
}

2-SAT

参数a,x,b,y代表xa为x或xb为y x,y是bool 是的我知道这个变量起的很弱智

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
//P4782
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e6+10;
const int maxm = 4E6+10;
struct Edge{
int from,to,next;
}edge[maxm],edgeT[maxm];
vector<int>r_topo;
int scc_no[maxn],scc_cnt;
bool vis[maxn];
int cnt = 1,cntT = 1,head[maxn],headT[maxn],n,m,q;
void addedge(int f,int t){
edge[cnt].from = f;
edge[cnt].to = t;
edge[cnt].next = head[f];
head[f] = cnt++;
edgeT[cntT].from = t;
edgeT[cntT].to = f;
edgeT[cntT].next = headT[t];
headT[t] = cntT++;
}
void DFS1(int u){
if(vis[u])return;
vis[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
DFS1(edge[i].to);
}
r_topo.push_back(u);
}
void DFS2(int u){
if(scc_no[u])return;
scc_no[u] = scc_cnt;
for(int i = headT[u];~i;i = edgeT[i].next)DFS2(edgeT[i].to);
}
void findscc(){
for(int i = 1;i <= 2*n;i++)DFS1(i);
for(int i = 2*n-1;i >= 0;i--){
if(!scc_no[r_topo[i]]){
scc_cnt++;
DFS2(r_topo[i]);
}
}
}
int main(){
// freopen("P4782.in","r",stdin);
memset(head,-1,sizeof(head));
memset(headT,-1,sizeof(headT));
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i++){
int a,b,x,y;
scanf("%d%d%d%d",&a,&x,&b,&y);
addedge(a+n*(x&1),b+n*(y^1));
addedge(b+n*(y&1),a+n*(x^1));
}
findscc();
bool ok = 1;
for(int i = 1;i <= n;i++)if(scc_no[i] == scc_no[i+n]){
ok = 0;
break;
}
if(!ok)printf("IMPOSSIBLE");
else{
printf("POSSIBLE\n");
for(int i = 1;i <= n;i++)printf("%d ",scc_no[i] > scc_no[i+n]);
}
return 0;
}

弦图

定义

子图: 点集和边集均为原图点集和边集子集的图.

导出子图: 点集为原图点集的子集, 且边集中所有边的两个端点都在导出子图的点集中.

团: 是完全图的子图

单纯点: 设N(x)为所有与x相邻的点的集合, 若{x}+N(x)的导出子图是团, 则x是单纯点

完美消除序列

此处完美消除序列(Perfect Elimination Ordering)的定义为: 令 n=|V|,完美消除序列 v1,v2,,vn1,2,,n 的一个排列,满足 vi{vi,vi+1,,vn} 的导出子图中为单纯点.

注意该定义可能与部分论文顺序相反.

最大势算法(MCS)

优先队列做法

已弃用, 有可能RE

复杂度: O((n+m)logn)

输入: n, 点数

head[],edge[],cnt, 无向图(全局变量)

使用的全局变量:vis[]

输出: vector<int>, 长度为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
vector<int> MCS(int n){
vector<int>res;
priority_queue<pair<int,int> >pq;
for(int i = 1;i <= n;i++){
label[i] = vis[i] = 0;
pq.push({0,i});
}
for(int step = 1;step <= n;step++){
while(pq.size()){
int u = pq.top().second;
pq.pop();
if(!vis[u]){
res.push_back(u);
vis[u] = 1;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
label[v]++;
pq.push({label[v],v});
}
break;
}
}
}
reverse(res.begin(),res.end());
return res;
}

链表做法

复杂度O(n+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
vector<int> MCS(int n){
int best = 0;
vector<int>res;
vector<list<int> >link(n+1);
for(int i = 1;i <= n;i++){
label[i] = vis[i] = 0;
link[0].push_back(i);
}
for(int step = 1;step <= n;step++){
while(best >= 0){
if(!link[best].size()){
best--;
continue;
}
int u = link[best].front();
link[best].pop_front();
if(!vis[u]){
vis[u] = 1;
res.push_back(u);
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(!vis[v]){
label[v]++;
link[label[v]].push_back(v);
if(label[v] > best)best = label[v];
}
}
}
}
}
reverse(res.begin(),res.end());
return res;
}

判断完美消除序列是否合法

根据完美消除序列的定义,设 vivi,vi+1,,vn 中相邻的点从小到大为 {vc1,vc2,,vck},则只需判断 vc1 与其他点是否直接连通即可

输入:int n-图的点数 vector<int> & peo-完美消除序列

输出: 该序列是否合法

时间复杂度O(mlogn+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
bool chord_check(int n,vector<int> & peo){
static bool mark[maxn];
static int rank[maxn];
for(int i = 0;i < n;i++)rank[peo[i]] = i,mark[i] = 0;
for(int u:peo){
vector<pair<int,int> >tmp;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(!mark[v])tmp.push_back({rank[v],v});
}
sort(tmp.begin(),tmp.end());
if(tmp.size()){
int u = tmp[0].second;
set<int>tmpadj;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
tmpadj.insert(v);
}
for(int i = 1;i < (int)tmp.size();i++){
if(!tmpadj.count(tmp[i].second))return 0;
}
}
mark[u] = 1;
}
return 1;
}

弦图最大独立集/最小团覆盖

最大独立集: 从前往后遍历完美消除序列, 每次选取不在独立集且与独立集无邻边的点加入独立集中.

最小团覆盖: 设最大独立集为 {v1,v2,,vt}, 则团的集合{ {v1+N(v1)},{v2+N(v2)},...,{vt+N(vt)} }为图的最小团覆盖. 时间复杂度均为 O(n+m)

弦图的色数/弦图的团数

一种构造方法:按完美消除序列从后往前依次给每个点染色,给每个点染上可以染的最小颜色。时间复杂度 O(m+n)

正确性证明:设以上方法使用了 t 种颜色,则 tχ(G)。由于团上每个点都是不同的颜色,所以 t=ω(G),由 Lemma 1t=ω(G)χ(G)。综上,可得 t=χ(G)=ω(G)

无需染色方案,只需求出弦图的色数/团数时,可以取 |{x}+N(x)| 的最大值得到。

最小生成树

结论

  • 同一个图的任意最小生成树上, 每种权值的边的数量是固定的.
  • 切分定理:对于给定图的任意割,横跨两个割集的边中权值严格最小的边必定在最小生成树上
  • 假设权值为w的边需要k条来形成最小生成树且已经加入k条, 那么无论选择了哪些合法的边, 图的联通状态都是相同的

Prim

dis[i]表示第i条树边的长度,标号从2n.

d[i][j]为边<i,j>权值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int prim(int n){
for(int i = 0;i <= n;i++)vis[i] = 0,dis[i] = INF;
for(int i = 1;i < n;i++){
int x = 0;
for(int j = 1;j <= n;j++)if(!vis[j]){
if(!x or dis[x] > dis[j])x = j;
}
vis[x] = 1;
for(int j = 1;j <= n;j++)if(!vis[j]){
dis[j] = min(dis[j],d[x][j]);
}
}

ll tans = 0;
for(int i = 2;i <= n;i++){
tans += dis[i];
}
return tans;
}

欧拉序

定义

在dfs整个树的过程中, 第一次访问或回溯时均记录下经过的节点即为欧拉序. 例:

image-20211109105112159

这棵树的欧拉序(根节点为1)为1 2 3 4 3 7 3 2 5 6 5 2 1

一棵n个节点的树欧拉序长度为2n1. (n个点, n1条边)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int clk;//下标指针
int euler[maxn],dep[maxn],fir[maxn],lst[maxn];//欧拉序, 深度, 节点第一次(最后一次)在欧拉序中出现的位置
void dfs(int u,int fa){
euler[++clk] = u;
fir[u] = lst[u] = clk;
dep[u] = dep[fa] + 1;
for(int v:G[u]){
if(v != fa){
dfs(v,u);
euler[++clk] = u;
lst[u] = clk;
}
}
}

将树上问题转化为区间问题

欧拉序可将一棵树映射为一维的区间. [fir[u],lst[u]]之间的欧拉序对应的是以u为根的子树的区间.

求解LCA

O(nlogn)预处理, O(1)查询. u,v之间的LCA即为欧拉序[fir[u],fir[v]]之间深度最小的点.

ST表中保存的是点的编号, 但按点的深度来比较.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void ST_init(int n){
lg[0] = -1;
for(int i = 1;i <= n;i++)ST[i][0] = euler[i];
for(int i = 1;i < maxn;i++)lg[i] = lg[i>>1] + 1;
for(int j = 1;(1<<j) <= n;j++)
for(int i = 1;i +(1<<j) - 1<= n;i++){
int v1 = ST[i][j - 1],v2 = ST[i + (1<<(j - 1))][j - 1];
ST[i][j] = dep[v1] < dep[v2]?v1:v2;
}
}
int query(int u,int v){
if(fir[u] > fir[v])swap(u,v);
u = fir[u],v = fir[v];
int k = lg[v-u+1];
int v1 = ST[u][k],v2 = ST[v - (1<<k) + 1][k];
return dep[v1] < dep[v2]?v1:v2;
}

DFS序

image-20211109105112159

上图的DFS序(不唯一)为1 2 3 4 4 7 7 3 5 6 6 5 2 1

一棵大小为n的树DFS序长度为2n.

最近公共祖先(LCA)-倍增

预处理O(n), 单次查询O(log)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void pre(int u,int fa){
if(u == fa)return;
dep[u] = dep[fa] + 1;
pa[u][0] = fa;
for(int i = 1;(1<<i) <= dep[u];i++)pa[u][i] = pa[pa[u][i-1]][i-1];
for(auto [v,w]:G[u])if(fa != v)pre(v,u);
}
int LCA(int a,int b){
if(dep[a] > dep[b])swap(a,b);
for(int i = 30;i >= 0;i--){
if(dep[a] <= dep[b] - (1<<i))b = pa[b][i];
}
if(a == b)return a;
for(int i = 30;i >= 0;i--){
if(pa[a][i] == pa[b][i])continue;
else{
a = pa[a][i];b = pa[b][i];
}
}
return pa[a][0];
}

重心

w[u]为删去u后剩余连通分量的最大值, rt为树的重心

树最多有两个相邻的重心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void cent(int u,int fa){
siz[u] = 1;
w[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].to;
if(v != fa){
cent(v,u);
siz[u] += siz[v];
w[u] = max(w[u],siz[v]);
}
}
w[u] = max(w[u],n - siz[u]);
if(!rt or w[u] < w[rt])rt = u;
}

最大独立集

树也是二分图, 最大独立集可以在多项式时间内求.

例题: 求最大带权独立集.

写法1

w[]是点权, ans[u]代表以u为根的子树最大带权独立集权值和, fagr分别代表u的父亲和祖父. f[u][0]u所有儿子节点ans的和, f[u][1]u所有孙子节点ans的和.

1
2
3
4
5
6
7
8
void dfs(int u,int fa,int gr){
for(int v:G[u])if(v != fa){
dfs(v,u,fa);
}
ans[u] = max(f[u][0],f[u][1] + w[u]);
f[fa][0] += ans[u];
f[gr][1] += ans[u];
}

写法2(方便扩展和还原方案)

f[u][0]表示u不在独立集里的最优答案, f[u][1]表示u在独立集里的最优答案.

1
2
3
4
5
6
7
8
9
10
int dfs(int u,int fa,int c){
if(f[u][c] != -1)return f[u][c];
int x = (c?w[u]:0);
for(int v:G[u])if(v != fa){
int r = dfs(v,u,0);
if(!c)r = max(r,dfs(v,u,1));
x += r;
}
return f[u][c] = x;
}

DMST/最小树形图/有向图最小生成树

Edmond's Algorithm. 代码来自HDU6990的官方题解.

输入: es[] = edge(起点, 终点, 权值)

调用solve(顶点数, 边数)

输出: ans[], ans[i]代表以i为根的DMST权值(即i到其它点都有一条路径可达), 若不存在则为1

时间复杂度O(mlogn)

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 DMST{
const ll inf = 4e13;
struct edge { int u, v; ll w; } es[maxn];
int ls[maxn], rs[maxn], dis[maxn];
ll val[maxn], tag[maxn];
void update(int x, ll t) { val[x] += t;tag[x] += t; }
void push_down(int x) {
if (ls[x]) update(ls[x], tag[x]);
if (rs[x]) update(rs[x], tag[x]);
tag[x] = 0;
}

int merge(int x, int y) {
if (!x || !y) return x | y;
if (val[x] > val[y]) swap(x, y);
push_down(x);
rs[x] = merge(rs[x], y);
if (dis[ls[x]] < dis[rs[x]]) swap(ls[x], rs[x]);
dis[x] = dis[rs[x]] + 1;
return x;
}
int top[maxn], fa[maxn], ine[maxn];
int findset[maxn]; int find(int x) { return findset[x] ? findset[x] = find(findset[x]) : x; }
vector<int> ch[maxn];

ll ans[maxn];
void dfs(int u, ll s) {
if (ch[u].empty())
ans[u] = s >= inf ? -1 : s;
else for (int v : ch[u])
dfs(v, s - val[ine[v]]);
}

void solve(int n, int m) {
for (int i = 1; i <= 2 * n; ++i) top[i] = fa[i] = ine[i] = findset[i] = 0, ch[i].clear();
for (int i = 1; i <= n; ++i) es[++m] = { i % n + 1, i, inf };
for (int i = 1; i <= m; ++i) {
ls[i] = rs[i] = tag[i] = dis[i] = 0;
val[i] = es[i].w;
top[es[i].v] = merge(top[es[i].v], i);
}
int x = 1;
while (top[x]) {
int i = top[x], y = find(es[i].u);
top[x] = merge(ls[i], rs[i]);
if (y == x) continue;
ine[x] = i;
if (!ine[es[i].u]) x = y;
else for (int z = ++n; x != z; x = find(es[ine[x]].u)) {
fa[x] = findset[find(x)] = z;
ch[z].push_back(x);
if (top[x]) update(top[x], -val[ine[x]]);
top[z] = merge(top[z], top[x]);
}
}

ll sum = 0;
for (int i = 1; i <= n; ++i)
sum += val[ine[i]];
dfs(n, sum);
}
}dmst;

树同构

AHU算法

用于解决有根树同构问题, 可以将每个子树映射到一个编号, 编号(hsh[])相等则同构. 是一个确定性算法. 下面的实现是O(nlogn)的但可优化至O(n). 使用时, 将两棵树存在同一个图中.

对于无根树A, B, 求出重心后分情况讨论:

  • 重心数量不同, 不同构.
  • 均只有一个重心, 以重心为根求解, 编号相同则同构.
  • 均只有两个重心, 若树A以重心1为根的编号, 和树B以重心1重心2为根的编号相同, 则同构.

求解编号时, 预处理出树的深度. 按深度由大到小一层层计算. 每个节点的编号信息为其所有叶子节点的编号排序之后的数组. 将该数组映射便得到该节点的编号.

下面的实现默认两个子树大小相等, 且树B的节点为原节点+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
#define ALL(x) (x).begin(),(x).end()
int hsh[maxn],dep[maxn];
map<vector<int>,int> id;
vector<int>rd[maxn];
int cid;
void cal_dep(int u,int fa){
dep[u] = (fa == -1?1:dep[fa] + 1);
for(int v:G[u])if(v != fa)cal_dep(v,u);
}
in
void cal(int r1,int r2){
cid = 0;id.clear();
for(int i = 1;i <= n*2;i++)dep[i] = 0;
cal_dep(r1,-1);
cal_dep(r2,-1);
for(int i = 1;i <= n*2;i++)rd[i].clear(),hsh[i] = 0;
for(int i = 1;i <= n*2;i++)rd[dep[i]].push_back(i);
for(int i = n;i >= 1;i--){
for(int u:rd[i]){
vector<int>tmp;
for(int v:G[u])tmp.push_back(hsh[v]);
sort(ALL(tmp));
int x = 0;
if(id.count(tmp))x = id[tmp];
else x = id[tmp] = ++cid;
hsh[u] = x;
}
}
}

也可以用递归写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int hsh[maxn];
map<vector<int>,int> id;
int cid;
int dfs(int u,int fa){
vector<int>cur;
for(int v:G[u])if(v != fa){
dfs(v,u);
cur.push_back(hsh[v]);
}
sort(ALL(cur));
int & tid = id[cur];
if(!tid)tid = ++cid;
hsh[u] = tid;
return tid;
}

判断无根树是否同构:

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
int __;
cin >> __;
while(__--){
cin >> n;
cid = 0;
id.clear();
for(int i = 0;i <= n*2;i++)siz[i] = w[i] = hsh[i] = 0,G[i].clear();
for(int i = 1;i < n;i++){
int f,t;
cin >> f >> t;
G[f].push_back(t);
G[t].push_back(f);
}
for(int i = 1;i < n;i++){
int f,t;
cin >> f >> t;
f += n,t += n;
G[f].push_back(t);
G[t].push_back(f);
}
int rt1 = 0,rt2 = 0;
cent(1,0,rt1);
cent(n+1,0,rt2);
int xrt1 = -1,xrt2 = -1;
for(int v:G[rt1])if(w[v] == w[rt1])xrt1 = v;
for(int v:G[rt2])if(w[v] == w[rt2])xrt2 = v;
dfs(rt1,-1);
dfs(rt2,-1);
bool ok = 1;
if((xrt1 == -1 and xrt2 != -1) or (xrt2 == -1 and xrt1 != -1))ok = 0;
else{
if(xrt1 == -1){
if(hsh[rt1] != hsh[rt2])ok = 0;
}
else{
if(hsh[rt1] != hsh[rt2]){
for(int i = 1;i <= n*2;i++)hsh[i] = 0;
cid = 0;id.clear();
dfs(rt1,-1);
dfs(xrt2,-1);
if(hsh[rt1] != hsh[xrt2])ok = 0;
}
}
}
cout << (ok?"YES\n":"NO\n");
}

点分治

具体看lyd的书.这里只放几道例题

例题1-Tree

求树上间距小于等于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
//P4178
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
const int maxn = 4e4+10;
const int maxm = 4e5+10;
int head[maxn],vis[maxn],cnt;
int dis[maxn],bal[maxn],siz[maxn];//权值,平衡因子,子树大小
int mapp[maxn],par[maxn],occur[maxn],tot;//树上点的映射,该点属于根的哪颗子树,出现次数,待处理的总数
int root,n,k,ans;
struct Edge{
int f,t,w,next;
Edge(int f = 0,int t = 0,int w = 0,int next = 0):f(f),t(t),w(w),next(next){}
}edge[maxm];
void addedge(int f,int t,int w){
edge[cnt] = Edge(f,t,w,head[f]);
head[f] = cnt++;
}
bool cmp(int a,int b){
return dis[a] < dis[b];
}
void get_root(int u,int fa,int rawsiz){
siz[u] = 1,bal[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v] or v == fa)continue;
get_root(v,u,rawsiz);
siz[u] += siz[v];
bal[u] = max(bal[u],siz[v]);
}
bal[u] = max(bal[u],rawsiz - siz[u]);
if(!root or bal[root] > bal[u])root = u;
}
void get_dis(int u,int fa,int w,int parent){
mapp[++tot] = u;
dis[u] = w,par[u] = parent;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v] or v == fa)continue;
get_dis(v,u,w + edge[i].w,parent);
}
}
void Cal(int u){
tot = 0;
mapp[++tot] = u;
dis[u] = 0,par[u] = u;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
get_dis(v,u,edge[i].w,v);
}
sort(mapp+1,mapp+tot+1,cmp);
// cout << tot << endl;
int l = 1,r = tot;
for(int i = 1;i <= tot;i++)occur[par[mapp[i]]]++;
while(l <= r){
if(dis[mapp[l]] + dis[mapp[r]] > k)occur[par[mapp[r--]]]--;
else{
ans += (r - l + 1 - occur[par[mapp[l]]]);
occur[par[mapp[l++]]]--;
}
}
}
void Divide(int u){
vis[u] = 1;
dis[u] = 0;
Cal(u);
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
root = 0;
get_root(v,-1,siz[v]);
Divide(root);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
memset(head,-1,sizeof(head));
cin >> n;
for(int i = 1;i < n;i++){
int f,t,w;
cin >> f >> t >> w;
addedge(f,t,w);
addedge(t,f,w);
}
cin >> k;
get_root(1,-1,n);
Divide(root);
cout << ans;
return 0;
}

例题2-[IOI2011]Race

给出一棵树,求路径和为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
//P4149
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\n'
#define int long long
const int maxn = 2e6+10;
const int maxm = maxn*2;
const int INF = 1E9;
int head[maxn],vis[maxn],dis[maxn],dep[maxn];
int bal[maxn],siz[maxn],par[maxn];
int mapp[maxn],mindis[maxn];
int cnt,ans,root,n,k,tot;
struct Edge{
int f,t,w,next;
Edge(int f = 0,int t = 0,int w = 0,int next = 0):f(f),t(t),w(w),next(next){}
}edge[maxm];
void addedge(int f,int t,int w){
edge[cnt] = Edge(f,t,w,head[f]);
head[f] = cnt++;
}
bool cmp(int a,int b){
return dis[a] < dis[b];
}
void get_root(int u,int fa,int rawsiz){
siz[u] = 1,bal[u] = 0;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v] or v == fa)continue;
get_root(v,u,rawsiz);
siz[u] += siz[v];
bal[u] = max(bal[u],siz[v]);
}
bal[u] = max(bal[u],rawsiz - siz[u]);
if(!root or bal[root] > bal[u])root = u;
}
void get_dis(int u,int fa,int parent){
par[u] = parent;
mapp[++tot] = u;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v] or fa == v)continue;
dis[v] = dis[u] + edge[i].w;
dep[v] = dep[u] + 1;
get_dis(v,u,parent);
}
}
void Cal(int u){
tot = 0;
int pretot = 0;
dep[u] = dis[u] = 0,par[u] = u;
mindis[0] = 0;
mapp[++tot] = u;
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
pretot = tot;
dis[v] = edge[i].w;
dep[v] = 1;
get_dis(v,u,v);
for(int j = pretot+1;j <= tot;j++){
int x = mapp[j];
if(k - dis[x] >= 0)ans = min(ans,mindis[k - dis[x]] + dep[x]);
}
for(int j = pretot+1;j <= tot;j++){
int x = mapp[j];
if(dis[x] < maxn)mindis[dis[x]] = min(mindis[dis[x]],dep[x]);
}
}
for(int i = 1;i <= tot;i++){
int j = mapp[i];
if(dis[j] < maxn)mindis[dis[j]] = INF;
}
}
void Divide(int u){
vis[u] = 1;
dis[u] = 0;
dep[u] = 0;
Cal(u);
for(int i = head[u];~i;i = edge[i].next){
int v = edge[i].t;
if(vis[v])continue;
root = 0;
get_root(v,-1,siz[v]);
Divide(root);
}
}
signed main(){
// freopen("P4149_1.in","r",stdin);
ios::sync_with_stdio(0);
cin.tie(0);
memset(head,-1,sizeof(head));
for(int i = 0;i < maxn;i++)mindis[i] = INF;
ans = INF;
cin >> n >> k;
for(int i = 1;i < n;i++){
int f,t,w;
cin >> f >> t >> w;
f++;t++;
addedge(f,t,w);
addedge(t,f,w);
}
get_root(1,-1,n);
Divide(root);
if(ans == INF)ans = -1;
cout << ans;
return 0;
}

虚树

虚树(auxiliary tree)主要用来解决一类树形dp问题. 这类树形dp每次求解只需考虑树中的部分点(称为关键点). 一棵虚树包含全部的关键点以及任意两个关键点之间的LCA. 可以证明k个关键点生成的虚树大小不大于2k.

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
vector<pair<int,int> >G[maxn],G2[maxn];//G2为虚树
int dfn[maxn],clk;
void dfs(int u,int fa){
dfn[u] = ++clk;
for(auto [v,ww]:G[u])if(v != fa){
w[v] = min(w[u],ww);
dfs(v,u);
}
}
bool aux_cmp(int u,int v){
return dfn[u] < dfn[v];
}
vector<int>del_tree;//这里包含了虚树中的所有点
void addedge(int f,int t){
//在此实现虚树加边
}
void build_aux_tree(vector<int> & key){
for(int x:del_tree)G2[x].clear(),G2[x].shrink_to_fit();
del_tree.clear();
vector<int>stk;
stk.push_back(1);
sort(ALL(key),aux_cmp);
for(int u:key){
if(stk.size() == 1){
stk.push_back(u);
continue;
}
int x = stk.back();
int lca = LCA(u,x);
if(lca == x){
stk.push_back(u);
}
else{
while(stk.size() > 1){
stk.pop_back();
int y = stk.back();
if(dfn[y] > dfn[lca]){
addedge(y,x);
x = y;
}
else if(dfn[y] == dfn[lca]){
addedge(lca,x);
break;
}
else{
addedge(lca,x);
stk.push_back(lca);
break;
}
}
stk.push_back(u);
}
}
if(stk.size() > 1){
int x = stk.back();
while(stk.size() > 1){
stk.pop_back();
addedge(stk.back(),x);
x = stk.back();
}
}
}