图论
模拟赛考到最大独立集我不知道是啥。。。我错了,我忏悔,我复习图论
基础概念
(不全,只含本人较为生疏并且认为可能有用的概念。资料源于oi-wiki)
阶: 图的点数。
相邻: 若存在边 \((u,v)\),则称 \(u,v\) 相邻。一个点的邻域是所有与之相邻的顶点构成的集合。
简单图:无重边和自环。具有至少两个顶点的简单无向图中一定存在度数相同的点(证明时分讨含有0,1,2个度数为0的点的情况,用抽屉原理)。
途径、迹、路径、回路。环:对于一条途径 \(w\) ,若 \(e1,e2...ek\) 互不相同,则 \(w\) 是一条迹。若一条迹,连接的点序列中所有点互异,则 \(w\) 是一条路径。对于一条迹 \(w\) ,若 \(v0=vk\) ,则 \(w\) 是一条回路。对于一条回路,若 \(v0=vk\) 是点序列中唯一重复出现的点对,则称 \(w\) 是一个环。(注意定义是否可含重复点、边)
导出/诱导子图: 对于 \(H \subseteq G\),\(\forall u, v \in V'\),只要 \((u, v) \in E\),均有 \((u, v) \in E'\)。
生成/支撑子图:\(H \subseteq G\) 满足 \(V' = V\)。
k- 正则图:对一张无向图 \(G = (V, E)\),每个顶点的度数都是一个固定的常数 k。
k- 因子:对于一张无向图 G 的某个生成子图 F ,且它为 k- 正则图。
闭合子图:有向图 \(G = (V, E)\) 的导出子图 \(H = G \left[ V^\ast \right]\) 满足 \(\forall v \in V^\ast\), \((v, u) \in E\),有 \(u \in V^\ast\) 。
补图:记作 \(\bar G\),满足 \(V \left( \bar G \right) = V \left( G \right)\) ,且对任意节点对 \((u, v)\),\((u, v) \in E \left( \bar G \right)\) 当且仅当 \((u, v) \notin E \left( G \right)\) 。
反图:点集不变,每条边反向。
完全图:无向图任意不同两点间均有边。
有向完全图:有向图任意不同两点都有两条方向不同的边。
竞赛图:有向简单图中任意不同两点之间都恰好有一条单向边。
平面图:一张图可以画在一个平面上,且没有两条边在非端点处相交
轮图:无向简单图 \(G = \left( V, E \right)\) 满足,存在一个点 v 为支配点(度数V-1),其它点之间构成一个圈。
仙人掌: 一张无向连通图的每条边最多在一个环内。多棵仙人掌可以组成沙漠。
交:图 \(G = \left( V_1, E_1 \right)\), \(H = \left( V_2, E_2 \right)\) 的交定义成图 \(G \cap H = \left( V_1 \cap V_2, E_1 \cap E_2 \right)\)。(容易证明两个无向简单图的交还是无向简单图。)
并:图 \(G = \left( V_1, E_1 \right)\), \(H = \left( V_2, E_2 \right)\) 的并定义成图 \(G \cup H = \left( V_1 \cup V_2, E_1 \cup E_2 \right)\)。
和/直和 :对于 \(G = \left( V_1, E_1 \right), H = \left( V_2, E_2 \right)\),任意构造 \(H' \cong H\) 使得 \(V \left( H' \right) \cap V_1 = \varnothing\) (H' 可以等于 H)。此时与 \(G \cup H'\) 同构的任何图称为 G 和 H 的和/直和/不交并,记作 \(G + H\) 或 \(G \oplus H\)。
支配集:对于无向图 G=(V, E),若 \(V'\subseteq V\) 且 \(\forall v\in(V\setminus V')\) 存在边 \((u, v)\in E\) 满足 \(u\in V'\),则 V' 是图 G 的一个支配集。也就是某个集起到了支配点的作用。
有向图中分为 入-支配集 和 出-支配集。即存在点集之间的边是指向V'还是指出v'。
边支配集:类似的,对于图 \(G=(V, E)\),若 \(E'\subseteq E\) 且 \(\forall e\in(E\setminus E')\) 存在 E' 中的边与其有公共点,则称 E' 是图 G 的一个边支配集。
独立集:对于原图 \(G=(V, E)\),若\(V'\subseteq V\) 且 V' 中任意两点都不相邻,则 V' 是图 G 的一个独立集。
匹配:对于图 \(G=(V, E)\),若 \(E'\in E\) 且 E' 中任意两条不同的边都没有公共的端点,且 E' 中任意一条边都不是自环,则 E' 是图 G 的一个 匹配,也可以叫作 边独立集。如果一个点是匹配中某条边的一个端点,则称这个点是 被匹配的 /饱和的,否则称这个点是不被匹配的。
边数最多的匹配被称作一张图的最大匹配
如果边带权,那么权重之和最大的匹配被称作一张图的最大权匹配
如果一个匹配在加入任何一条边后都不再是一个匹配,那么这个匹配是一个极大匹配
如果在一个匹配中所有点都是被匹配的,那么这个匹配是一个完美匹配。若只有一个点不被匹配,那么这个匹配是一个准完美匹配。
点覆盖:对于图 \(G=(V, E)\),若 \(V'\subseteq V\) 且 \(\forall e\in E\) 满足 e 的至少一个端点在 V' 中,则称 V' 是图 G 的一个点覆盖。
点覆盖集必为支配集,但极小点覆盖集不一定是极小支配集(因为支配集外还可能有连边,而点覆盖一定是把边覆盖完了)。
一个点集是点覆盖的充要条件是其补集是独立集,因此最小点覆盖的补集是最大独立集。
一张图的任何一个匹配的大小都不超过其任何一个点覆盖的大小。完全二分图 \(K_{n, m}\) 的最大匹配和最小点覆盖大小都为 \(\min(n, m)\)。
边覆盖:对于图 \(G=(V, E)\),若 \(E'\subseteq E\) 且 \(\forall v\in V\) 满足 v 与 E' 中的至少一条边相邻,则称 E' 是图 G 的一个 边覆盖。
最小边覆盖可以由最大匹配贪心求:把所有非匹配点的一条邻边加入最大匹配。反之,最大匹配也可以由最小边覆盖求。
最小边覆盖大小+最大匹配大小=图的点数
最大匹配 \(leq\) 最小边覆盖,当且仅当完美匹配时取等。
独立集 \(leq\) 边覆盖
团:若一个子图,其中任意两个不同顶点相邻,则它是原图的一个团。团的导出子图是完全图。
若一个团在加入任何一个顶点之后都不再是一个团,那么它是极大团。
最大团大小=其补图的最大独立集
tarjan
复习
有向图中:
若一张有向图的节点两两互相可达,则称这张图是强连通的(对应有强联通分量)
若一张有向图的边替换为无向边后可以得到一张连通图,则称原来这张有向图是弱连通的
常用缩点,之后可以形成DAG,可以拓扑排序。
无向图中:
割点,即删去该点及其所连边时图将被分成两个及以上的不相连的子图;注意两种情况,第一种是该节点为根节点且子节点数大于等于二;第二种是该节点不为根节点且low[y]>=dfn[x],即子节点y可以回溯到的最早节点不早于x,那么删去x一定会导致x的父亲和x不连通,反之,low[y]<dfn[x]说明它能绕行到其他比x更早访问的节点,就不是割点了
割边(桥),即删去该边时图将被分成两个及以上的不相连的子图;判断割边时对于(x,y)这条边low[y]>dfn[x]那么它才是割边,这是需要和割点区分的;注意存图的时候cnt=1,然后直接b[i]=b[i^1]=1记录割边;还要判断x-y当前边是不是上一条y到达x的边
点双,若一张无向连通图不存在割点,那么它为点双联通图,无向图的极大点上联通子图被称为点双联通分量;求点双时要拆点,图中任意一个割点都在至少两个点双中,任意一个不是割点的点都只存在一个点双中;点双缩点,每一个割点建点,每一个点双建点,然后根据从属关系连边,形成树的结构,可以跑dp
点双
void tarjan(int x,int anc){
int child=0;
dfn[x]=low[x]=++num;
s[++top]=x;
ins[x]=1;
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if(!dfn[y]){
child++;
tarjan(y,x);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
sum++;
while(s[top+1]!=y) ans[sum].push_back(s[top--]);//不要把割点出栈,因为还有可能是别的的点双里的
ans[sum].push_back(x);//把割点/树根也丢到点双里,可以看出这样判断树根是正确的
}
}
else if(y!=anc) low[x]=min(low[x],dfn[y]);
}
if(child==0&&anc==0) ans[++sum].push_back(x);//特判独立点
}
边双,图中不存在割边,其缩点类似强连通分量,遍历到dfn[x]==low[x]时,弹栈记录
2-sat
其实是一种构图的思想。
简单来说就是给出n个集合,每个集合有两个元素(i,i')。已知若干对关系<a,b>矛盾(a,b属于不同集合)。从每个集合中选择一个元素,判断能否一共选n个互不矛盾的元素。
一般过程就是对这些关系建立有向图,连边(x,y)表示若选择x则必须选择y,来使这些矛盾关系达到自洽。
连完边之后就是跑tarjan缩点,若集合内两个元素在同一个SCC(强连通分量)中,则不合法。输出方案时从叶节点(反拓扑序)向上跑,实际上是选择i与i'中拓扑序较大的一个就可以得到一组可行解。(可以理解为越下面限制条件越少)(另外,如果tarjan缩点,直接就是节点越靠近叶子节点编号越小,那么就是优先对所属连通块编号小的节点选择)
关于算法方案的正确性,可以考虑2-sat连边的对称性。
二分图
定义:无向图,点集可以分成两个互不相交的子集,且图中所有边的两个端点分别属于两个子集。(注意和2-sat的区别)
判定:黑白染色,不含奇环
二分图最大匹配(匈牙利算法) 时间复杂度O(nm)
#include<bits/stdc++.h>
using namespace std;
int n,m,w,tot,h[100005],ans,vis[505],mat[505];
struct node{
int to,nxt;
}e[100005];
void add(int u,int v){
e[++tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
bool dfs(int x){
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if(vis[y]) continue;
vis[y]=1;
if(!mat[y]||dfs(mat[y])){
mat[y]=x;
return 1;
}
}
return 0;
}
int main(){
scanf("%d%d%d",&n,&m,&w);
for(int i=1;i<=w;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
}
for(int i=1;i<=n;i++){
memset(vis,0,sizeof(vis));
if(dfs(i)) ans++;//每次寻找增广路且其合法长度一定为奇数
}
printf("%d",ans);
return 0;
}
套路
1.最大匹配:棋盘相邻格染色;棋盘以行/列或其能通达的块为点,其交点连边(TOJ 1306);拆点;配套二分答案使用(这部分问题用网络流可做)
2.最小点覆盖:在一个图中任意选取最少多少个点,可以保证图中所有边都和选取的点相连(并不只在二分图中存在)。
结论:二分图最小点覆盖=二分图最大匹配。
构造:先求出最大匹配,再从左边每个非匹配点出发dfs找增广路,标记访问节点,最后取左边没有打上标记的点和右边打上标记的点得到最小点覆盖。
证明:1)左边的非匹配点一定都被标记(出发点),右边的非匹配点一定都没有被标记(否则找到增广路);一对匹配点要么都被标记,要么都没被标记;那么取左边的非标记点和右边的标记点,正好是每条匹配边取了一个点,即为最大匹配
2)匹配边一定被覆盖(正好一个端点被取走);不存在连接两个非匹配点的边(否则存在增广路);连接左非匹配点右匹配点的边,右匹配点一定被访问,被选;连接左匹配点右非匹配点的边,左匹配点一定没被访问,被选。
3.最大独立集:任意两点在图中都没有边相连的点集。
结论:二分图最大独立集=图的点数-二分图最大匹配
证明:选出最多的点构成独立集,相当于选出最少的点覆盖所有的边,去掉的是最小覆盖。
4.最小路径覆盖:在一个有向图中找出最少的路径,使得这些路径经过所有点。
分类:1)最小不相交路径覆盖,即用尽量少的不相交简单路径覆盖有向无环图的所有顶点。
构造:把原图中每个点拆成二分图中左右两个点,对每条有向边(u,v),从u的左部点(出点)向v的右部点(入点)连一条有向边。
结论:最小路径覆盖数=原有向图节点数-新二分图最大匹配数
证明:首先,若最大匹配为0,则二分图中无边,即有向图G中无边,那么最小路径覆盖=|G|-0=|G|.若此时增加一条匹配边x1--x2
2)最小相交路径覆盖
基环树
最常见条件:n个点,n条边(注意题目给的条件是否联通,可能是森林
基环内向树:每个点只有一条出边
基环外向树:每个点只有一条入边
处理手法:找环(一定要把连通块跑完),断边,跑树形dp
点击查看代码
void circle(int x,int p){
vis[x]=1;
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if((i^1)==p) continue;
if(vis[y]){
xx=x,yy=y,k=i;
continue;//不能return,要把联通块找完
}
circle(y,i);
}
}
线段树优化建图
一看到点和区间或区间和区间连边的问题,大概率就是线段树优化建图了。
其基本思想就是自上而下连一个“出树”,自下而上连一个“入树”,对于其叶子节点(1-n)相互一一连边。然后按照题目要求从入树的节点/区间,连向出树的节点/区间。
一般建完图跑最短路的情况较多。
树链剖分/lca
考一遍,学一遍,忘一遍
这里是重链剖分。
两个dfs,第一个找重儿子,第二个找重链顶和dfn(注意要优先对重儿子dfs来保证同一条重链上的dfs序连续)
查询和维护时一个一个跳重链顶端,时间复杂度O(nlogn)。常和线段树配套使用。
模板
#include<bits/stdc++.h>
#define ll long long
#define lid (id<<1)
#define rid (id<<1|1)
using namespace std;
const int maxn=100005;
int tot,h[maxn<<1];
struct edge{
int to,nxt;
}e[maxn<<1];
void addedge(int u,int v){
e[++tot].to=v;
e[tot].nxt=h[u];
h[u]=tot;
}
int n,m,num,rt,size[maxn],f[maxn],dep[maxn],son[maxn],pre[maxn],dfn[maxn],top[maxn];
ll mod,a[maxn];
struct node{
int l,r; ll lazy,val;
}t[maxn<<2];
void dfs1(int x,int fa){
size[x]=1;
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if(y==fa) continue;
dep[y]=dep[x]+1;
f[y]=x;
dfs1(y,x);
size[x]+=size[y];
if(size[y]>size[son[x]]) son[x]=y;
}
}
void dfs2(int x,int tp){
dfn[x]=++num,top[x]=tp;pre[num]=x;
if(son[x]) dfs2(son[x],tp);
for(int i=h[x];i;i=e[i].nxt){
int y=e[i].to;
if(y==f[x]||y==son[x]) continue;
dfs2(y,y);
}
}
void build(int id,int l,int r){
t[id].l=l,t[id].r=r;
if(l==r){
t[id].val=a[pre[l]]%mod;
return ;
}
int mid=(l+r)>>1;
build(lid,l,mid);
build(rid,mid+1,r);
t[id].val=(t[lid].val+t[rid].val)%mod;
}
void pushdown(int id){
if(t[id].lazy&&t[id].l!=t[id].r){
t[lid].lazy=(t[lid].lazy+t[id].lazy)%mod;
t[rid].lazy=(t[rid].lazy+t[id].lazy)%mod;
t[lid].val=(t[lid].val+t[id].lazy*(t[lid].r-t[lid].l+1)%mod)%mod;
t[rid].val=(t[rid].val+t[id].lazy*(t[rid].r-t[rid].l+1)%mod)%mod;
t[id].lazy=0;
}
}
void add(int id,int l,int r,ll val){
if(t[id].l==l&&t[id].r==r){
t[id].lazy=(t[id].lazy+val)%mod;
t[id].val=(t[id].val+val*(t[id].r-t[id].l+1)%mod)%mod;
return;
}
pushdown(id);
int mid=(t[id].l+t[id].r)>>1;
if(r<=mid) add(lid,l,r,val);
else if(l>mid) add(rid,l,r,val);
else add(lid,l,mid,val),add(rid,mid+1,r,val);
t[id].val=(t[lid].val+t[rid].val)%mod;
}
ll query(int id,int l,int r){
if(t[id].l==l&&t[id].r==r){
return t[id].val;
}
pushdown(id);
int mid=(t[id].l+t[id].r)>>1;
if(r<=mid) return query(lid,l,r);
else if(l>mid) return query(rid,l,r);
else return (query(lid,l,mid)+query(rid,mid+1,r))%mod;
}
void qadd(int x,int y,ll k){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);//比的是top,否则可能跳多了……
add(1,dfn[top[x]],dfn[x],k);
x=f[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
add(1,dfn[y],dfn[x],k);
}
ll qsum(int x,int y){
ll ans=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
ans=(ans+query(1,dfn[top[x]],dfn[x]))%mod;
x=f[top[x]];
}
if(dep[x]<dep[y]) swap(x,y);
ans=(ans+query(1,dfn[y],dfn[x]))%mod;
return ans;
}
int main(){
scanf("%d%d%d%lld",&n,&m,&rt,&mod);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
}
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
addedge(u,v),addedge(v,u);
}
dfs1(rt,0);
dfs2(rt,rt);
build(1,1,n);
for(int i=1;i<=m;i++){
int tmp,x,y; ll z;
scanf("%d%d",&tmp,&x);
if(tmp==1){
scanf("%d%lld",&y,&z);
qadd(x,y,z%mod);
}
else if(tmp==2){
scanf("%d",&y);
printf("%lld\n",qsum(x,y));
}
else if(tmp==3){
scanf("%lld",&z);
add(1,dfn[x],dfn[x]+size[x]-1,z%mod);
// cout<<dfn[x]<<" "<<dfn[x]+size[x]-1<<endl;
}
else{
printf("%lld\n",query(1,dfn[x],dfn[x]+size[x]-1));
//cout<<dfn[x]<<" "<<dfn[x]+size[x]-1<<endl;
}
//cout<<i<<" "<<query(1,5,5)<<endl;
}
return 0;
}
性质:树上的每个节点都属于且仅属于一条重链;重链内的dfs序是连续的;一棵子树内的dfs序是连续的;当我们向下经过一条轻边时,所在子树大小至少除以2。
查询时间复杂度证明:重链的两边必是轻边,而每向下经过一条轻边,所在子树大小至少除以二,所以是log级别的。
另外,树剖可以求lca且一般跑不满,会比倍增lca快很多。(需要注意的是,最后top相等的时候,深度小的为lca。)当然欧拉序lca和黑科技dfs序lca的查询确实很快。
但倍增lca的优势在于它可以辅助dp,维护倍增信息,辅助二分等。
最后一点性质的应用:树上启发式合并。
dsu on tree
步骤:1.遍历节点u的轻儿子并计算答案,但不保留遍历后它对cnt数组的影响
2.遍历它的重儿子,保留它对cnt数组的影响
3.再次遍历u的轻儿子的子树节点,加入这些节点的贡献,得到u的答案
时间复杂度的证明,根节点到树上任意节点的轻边数不超过logn条,而一个节点被遍历的次数等于它到根节点路径上的轻边数+1,总时间复杂度O(nlogn)
例题:CF741D
码
//正解树上启发式合并+重链剖分:
//计算轻儿子答案不保留对cnt贡献-->计算重儿子答案并保留贡献-->再次遍历轻儿子子树节点
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,h[maxn],cnt,to[maxn],nxt[maxn],w[maxn],siz[maxn],son[maxn];
int id[maxn],l[maxn],r[maxn],num,dis[maxn],ans[maxn],t[1<<22],dep[maxn];
void add(int u,int v,int val){
nxt[++cnt]=h[u];
to[cnt]=v;
h[u]=cnt;
w[cnt]=(1<<val);
}
void pre(int x,int fa){
siz[x]=1,id[++num]=x,l[x]=num;
dep[x]=dep[fa]+1;
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
dis[y]=(dis[x]^w[i]);
pre(y,x);
siz[x]+=siz[y];
if(siz[y]>siz[son[x]]) son[x]=y;
} r[x]=num;
}
void dfs(int x,int tg){
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y==son[x]) continue;
dfs(y,0),ans[x]=max(ans[x],ans[y]);
}
if(son[x]) dfs(son[x],1),ans[x]=max(ans[x],ans[son[x]]);
if(t[dis[x]]) ans[x]=max(ans[x],t[dis[x]]-dep[x]);//特判重儿子,否则难以计算到全在这里的情况
for(int i=0;i<=21;i++) ans[x]=max(ans[x],t[dis[x]^(1<<i)]-dep[x]);
t[dis[x]]=max(t[dis[x]],dep[x]); //更新!否则都不能转移,全是0
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y==son[x]) continue;
for(int j=l[y];j<=r[y];j++){
int z=id[j];
if(t[dis[z]]) ans[x]=max(ans[x],t[dis[z]]+dep[z]-2*dep[x]);
for(int k=0;k<=21;k++){
if(t[dis[z]^(1<<k)]) ans[x]=max(ans[x],t[dis[z]^(1<<k)]+dep[z]-2*dep[x]);
}
}
for(int j=l[y];j<=r[y];j++){
int z=id[j];
t[dis[z]]=max(t[dis[z]],dep[z]);
}
}
if(!tg) for(int i=l[x];i<=r[x];i++) t[dis[id[i]]]=0;
}
int main(){
scanf("%d",&n);
for(int i=2;i<=n;i++){
int x;char val;
scanf("%d",&x);
scanf(" %c",&val);
add(x,i,(int)(val-'a'));
}
pre(1,1);
dfs(1,1);
for(int i=1;i<=n;i++){
printf("%d ",ans[i]);
}
return 0;
}
ps.长链剖分:好像是一个用来优化dp的东西,可以把空间做到O(n),还不会,要学
prufer序列
定义:一种将带编号的树用唯一的一个整数序列表示的方法,即可以把一颗带标号的 n 个节点的树序列化为一个长度为 n−2 的唯一的序列。
也就相当于完全图的生成树与数列之间的双射。
构造方法:每次选择一个编号最小的叶节点并删掉它,并把其父亲节点加入序列。
重复 n−2 次如上操作,此时树上只剩 2 个节点,算法结束。
首先找到编号最小的,度数为 1 的叶子节点,指针 p 指向它,将它删除。
如果删除了这个节点后,它的父亲节点的编号比它小并且父亲节点的度数也变成了1,那么此时这个节点一定是编号最小的叶子节点,删除它即可,同时不断往上删除,注意在这个过程中 p 并不进行移动。
否则,继续找到下一个编号最小的叶子节点即可。
每条边在删度数的时候最多被访问一次,而指针最多遍历每个结点一次,因此复杂度是 O(n) 的。
性质:1.构造完prufer序列后剩下两个节点其中一个是点n
2.每个节点出现次数是度数-1
3.没有出现在prufer里的节点就是叶子节点
4.对于给定度数为 \(d_{1,2,\ldots,n}\) 的一棵无根树共有 \(\frac{(n-2)!}{\prod_{i=1}^{n}(d_i-1)!}\) 种情况。
Cayley定理:有n个节点的完全图有 \(n^{n-2}\) 棵生成树。
因为任意一个长度为n-2的值域[1,n]的整数序列都可以通过prufer序列双射对应一个生成树。
扩展Cayley定理:
1.\(n\) 个标号节点形成一个有 \(s\) 颗树的森林且给定的 \(s\) 个点没有两个点属于同一颗树的方案数个数为 \(sn^{n-s-1}\) .
2.对于一棵 \(n\) 个节点有标号无根树,已经被若干条边分成了大小分别为若干联通块 \(a_1,a_2,...,a_m\) ,连成一棵树的方案数为 \(\prod a_i \times n^{m-2}\) .
树哈希
对于无标号无根树的枚举,可以先生成无标号有根树,再看它的根是否在重心上,用树哈希维护
树哈希就是一种枚举无标号有根树形态的算法
(最好不要直接用hash这个词,因为它是关键字)
//xor shift居然在换根时可以直接减掉子树哈希!
//异或时间是为了减少被卡概率
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
const int maxn=1e5+5;
const ull mask=std::chrono::steady_clock::now().time_since_epoch().count();
ull shift(ull x){
// x^=mask;
x^=x<<13;
x^=x>>7;
x^=x<<17;
// x^=mask;
return x;
}
int n,cnt,nxt[maxn<<1],to[maxn<<1],h[maxn<<1],b[maxn],fv[maxn];
void add(int u,int v){
nxt[++cnt]=h[u];
to[cnt]=v;
h[u]=cnt;
}
ull f[maxn],g[maxn];
set<ull>s;
void ppp(int x,int fa){
f[x]=1;
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y==fa) continue;
ppp(y,x);
f[x]+=shift(f[y]);
}
}
void ggg(int x,int fa){
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y==fa) continue;
g[y]=f[y]+shift(g[x]-shift(f[y]));
ggg(y,x);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
ppp(1,0);
g[1]=f[1];
ggg(1,0);
for(int i=1;i<=n;i++){
s.insert(g[i]);
// printf("%llu ",g[i]);
}
// printf("\n");
for(int i=1;i<=cnt;i++) nxt[i]=to[i]=h[i]=0;
cnt=0;
for(int i=1;i<=n;i++) f[i]=g[i]=0;
for(int i=1;i<=n;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
fv[u]=v,fv[v]=u;
b[u]++,b[v]++;
}
ppp(1,0);
g[1]=f[1];
ggg(1,0);
for(int i=1;i<=n+1;i++){
//注意这里输出是叶子节点,而且为了能够单独减去叶子,一定要在他的直系父节点!!
if(b[i]>1) continue;
// printf("%llu ",g[fv[i]]-shift(1));
if(s.count(g[fv[i]]-shift(1))){
printf("%d",i);
break;
}
}
return 0;
}
笛卡尔树
每个节点由一个键值二元组(k,w)构成。一种编号k满足二叉搜索树,而权值w满足堆的结构,经常在dp中用到(情形一般是把区间最值问题转化成树形dp,例 CF1580D Subsequence)
建树:将元素按下标顺序依次插入,由于它编号满足二叉搜索树,所以我们每次插入的元素必然在树的右链。用栈维护右链,从下往上比较右链节点与当前节点y的w,弹出节点,若找到一个右链上的节点x满足 \(w_x<w_y\) ,就把y接到x的右儿子上,而x原来的右子树变成y的左子树。
这个过程可以用栈维护,每个数最多进出右链一次,因此是O(n)的
剩下的用树形dp就行,而且还是二叉树,并且满足两个区间的lca就是这个大区间的最值,性质非常优秀
ps.实际上treap是笛卡尔树的一种
例
//化简式子之后好像是笛卡尔树的板子,经过复习,发现这个确实好用
//因为对于我们dp算最值的贡献,实际上就是合并子树
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=4005;
int n,m,a[maxn],siz[maxn],c[maxn][2],s[maxn],top,g[maxn],f[maxn][maxn];
void dfs(int x){
f[x][1]=(m-1)*a[x],siz[x]=1;
for(int p=0;p<2;p++){
if(!c[x][p]) continue;
int y=c[x][p];
dfs(y);
for(int i=1;i<=siz[x];i++) g[i]=f[x][i];
for(int i=0;i<=siz[x];i++){
for(int j=0;j<=siz[y];j++){
f[x][i+j]=max(f[x][i+j],g[i]+f[y][j]-2ll*a[x]*i*j);
}
}
siz[x]+=siz[y];
}
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
}
for(int i=1;i<=n;i++){//其实重点就是建树了
int k=top;
while(k&&a[s[k]]>=a[i]) k--;
if(k) c[s[k]][1]=i;
if(k<top) c[i][0]=s[k+1];
s[++k]=i; top=k;
}
dfs(s[1]);
printf("%lld",f[s[1]][m]);
return 0;
}
欧拉回路
即只有起点和终点是奇度数,其他都是偶度数,则能从起点到终点不重复地走完全部边。(特殊情况是起点和终点是同一个点,那么全部点都是偶度数。)可以理解为偶数是有入有出,奇数是只入不出或只出不入。
竞赛图
竞赛图是一个有向图,满足任意两个不同的点之间均存在且仅存在一条有向边相连。
也就是带方向的完全图。
性质:1.一定存在哈密顿通路(指经过图中每个点且仅经过一次的通路)
2.缩点后一定是一条链
考试题 竞赛图 手法:用出边的交集的子集去更新强连通子图为非强连通图,原因在于只有强连通分量内全是出边指向的才是非强连通,否则一定有入边指回。
分层图
主要是一个建图的事情,经常根据题目灵活变换。
每一层代表一种转移状态,方便跑最短路。
平面图和对偶图
平面图和对偶图的相互转化。简单来说(可能不严谨),平面图是边将图划分成为若干个不相交的面(比如网格),对偶图是把平面图的每个面化为一个点,如果两面相邻,则对该两点连一条边。经典手法是求平面图的划分数等价于对偶图的路径数。
最小生成树(MST)之Boruvka
Boruvka是Kruskal(加边O(mlogm))和Prim(加点,维护每个点的距离,朴素O(n^2))的结合算法,“博采众长”,因此能解决一些二者很难快速解决的问题,尤其是稠密图的最小生成树问题。
思想:最开始每个点都是一个孤立连通块,经过多轮迭代,把边两端的连通块合并。加边时对于每个连通块找到离它最近的连通块,则这条变时这个连通块的"最小边"。每一轮结束时将所拥有最小边的连通块的最小边加入MST边集,可以O(m)枚举边更新,不过通常用dp等方法更优地求出离它最近的连通块。
每一轮结束后,将所有连通块设为"无最小边"并进入下一轮。对于一个连通图,合并一轮至少将连通块的数量减半,因此最多logn轮,O(mlogn)。当然时间复杂度主要在于找最小边的方法。因此我们经常和树形dp、换根dp等一起用。
Kruskal重构树
发现我们每次加边的时候都是用并查集合并了两个点,如果我们每次都另设一个根节点,那么在n-1轮之后我们会得到一棵恰有n个叶子的二叉树。它具有很多优良的性质,并且我们可以在上面进行dp、倍增询问等操作。
性质:原图中两个点之间所有简单路径上最大边权的最小值=最小生成树上两个点之间的简单路径上的最大值=Kruskal重构树上两点之间LCA的权值
虚树
在多组询问的树形dp问题中,若暴力,则会达到O(nq)级别
那么虚树就是一种只保留需要dp的关键节点,并只计算关键节点贡献的算法。
建立就是先把节点按照dfs序排序,然后逐个插入,用单调栈维护,这个也是维护右链的过程,和笛卡尔树类似。注意插入时要枚举相邻的两个点的lca并连接,因为要保证祖先关系才好dp。还有一些insert时的小特判。
时间复杂度 \(O(\sum mlog_n)\).
//虚树:为解决多次询问少量节点,难以在整棵树上dp的情况
//甚至当前目标点的祖先上有目标点,当前点都不需要建
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1000005;
int n,m,q,cnt,h[maxn<<1],to[maxn<<1],nxt[maxn<<1],a[maxn],b[maxn];
int dep[maxn],fa[maxn][21],dfn[maxn],num,s[maxn],top,dis[maxn];
int mx,nx,id,idd,f[maxn];
ll ans,size[maxn];
vector<int>g[maxn];
void add(int u,int v){
nxt[++cnt]=h[u];
to[cnt]=v;
h[u]=cnt;
}
bool cmp(int x,int y){
return dfn[x]<dfn[y];
}
void dfs(int x,int anc){
dfn[x]=++num;
dep[x]=dep[anc]+1;
fa[x][0]=anc;
for(int i=1;i<=20;i++){
fa[x][i]=fa[fa[x][i-1]][i-1];
}
for(int i=h[x];i;i=nxt[i]){
int y=to[i];
if(y==anc) continue;
dfs(y,x);
}
}
int LCA(int x,int y){
if(dep[x]<dep[y]) swap(x,y);
for(int i=20;i>=0;i--){
if(dep[fa[x][i]]>=dep[y]) x=fa[x][i];
}
if(x==y) return x;
for(int i=20;i>=0;i--){
if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
void insert(int x){
if(top==0){ s[++top]=x; return ; }
int lca=LCA(x,s[top]);
if(lca==s[top]){ s[++top]=x; return ; }
while(top>1&&dep[s[top-1]]>=dep[lca]) g[s[top-1]].push_back(s[top]),g[s[top]].push_back(s[top-1]),top--;
if(s[top]!=lca) g[lca].push_back(s[top]),g[s[top]].push_back(lca),s[top]=lca;
s[++top]=x;
}
void ppp(int x,int anc){
f[x]=1e9;
if(b[x]) size[x]=1;
else size[x]=0;
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(y==anc) continue;
int len=abs(dep[y]-dep[x]);
dis[y]=dis[x]+len;
if(b[y]&&dis[y]>mx) mx=dis[y],id=y;
ppp(y,x);
size[x]+=size[y];
ans+=(ll)len*(m-size[y])*size[y];
}
}
void ggg(int x,int anc){
int mn1=1e8,mn2=1e8;
for(int i=0;i<g[x].size();i++){
int y=g[x][i];
if(y==anc) continue;
int len=abs(dep[y]-dep[x]);
dis[y]=dis[x]+len;
// printf("%d %d\n",dis[y],mx);
if(b[y]&&dis[y]>mx) mx=dis[y],idd=y;
ggg(y,x);
if(b[x]) nx=min(nx,f[y]+len);
else{
int tmp=f[y]+len;
if(tmp<=mn1) mn2=mn1,mn1=tmp;
else if(tmp<=mn2) mn2=tmp;
}
// printf("%d %d %d\n",x,y,nx);
}
if(b[x]) f[x]=0;
else f[x]=mn1,nx=min(nx,mn1+mn2);
g[x].clear();
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v); add(v,u);
}
dfs(1,0);
scanf("%d",&q);
while(q--){
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d",&a[i]);
b[a[i]]=1;
}
if(m<=1){
printf("0 0 0\n");
continue;
}
sort(a+1,a+m+1,cmp);
s[top=0]=0;
for(int i=1;i<=m;i++) insert(a[i]);
while(top>1) g[s[top-1]].push_back(s[top]),g[s[top]].push_back(s[top-1]),top--;
ans=0,mx=0,nx=1e8;
ppp(s[top],0);
for(int i=1;i<=m;i++) dis[a[i]]=0; mx=0;
ggg(id,0);
// printf("%d %d\n",id,idd);
mx=dep[id]+dep[idd]-2*dep[LCA(id,idd)];
for(int i=1;i<=m;i++) b[a[i]]=0,dis[a[i]]=0;
printf("%lld %d %d\n",ans,nx,mx);
}
return 0;
}
最大团
最大团即一个任意两点相邻的子图。
考试时遇到了一个用均值不等式推出来结论是求最大团的题。以下是最大团算法:
Bron–Kerbosch
设P表示有可能加入当前在找的极大团里的点,R表示当前正在找的极大团里的点,X表示已经找到的极大团里的点(用来判重,和时间复杂度无关)
每次搜索\(R\cup{u},P\cap nxt(u),X\cap nxt(u)\),\(nxt(u)\)表示和 \(u\) 相邻的点。其中能保证正确性的是P集合一定是在最大团中的相邻点集合的交,那么也就能保证加入的点一定和前面的点构成完全子图,P为零时是极大团,且X为0时代表还没有算过这个方案。
优化方面,可以加一个关键点优化,就是在P集合里,如果选择加入了某个点u,那么遍历到与之相邻的v时不需要加入,因为在u的递归层会遍历到。
据说时间复杂度是\(O(3^{n/3})\),但又据说跑得很快。
例题
#include<bits/stdc++.h>
using namespace std;
int n,m,t,a[45][45],vis[45][45],r[45][45],p[45][45],num,mx;
void bk(int d,int R,int P,int X){
if(!P&&!X){
mx=max(mx,R);
return ;
}
int u=p[d][1];
for(int i=1;i<=P;i++){
int v=p[d][i];
if(a[u][v]) continue;//实际上优化就这个
for(int j=1;j<=R;j++) vis[d+1][j]=vis[d][j];
vis[d+1][R+1]=v;
int np=0,nx=0;
for(int j=1;j<=P;j++){
if(a[v][p[d][j]]) p[d+1][++np]=p[d][j];
}
for(int j=1;j<=X;j++){
if(a[v][vis[d][j]]) vis[d+1][++nx]=vis[d][j];
}
bk(d+1,R+1,np,nx);
p[d][i]=0,vis[d][++X]=v;
}
}
int main(){
// freopen("nanami.in","r",stdin);
// freopen("nanami.out","w",stdout);
scanf("%d%d%d",&n,&m,&t);
for(int i=1,u,v;i<=m;i++){
scanf("%d%d",&u,&v);
a[u][v]=a[v][u]=1;
}
for(int i=1;i<=n;i++) p[1][++num]=i;
bk(1,0,num,0);
double ans=0.5*(1.0-1.0/mx)*t*t;
printf("%.6lf",ans);
return 0;
}