图论
最小生成树
(N)个城市,(M)条可修的公路,每条公路有一个修的成本(w_i),要使(N)个城市连通,所需要的最低成本?
最少需要(N-1)条边,构成一棵树。
Kruskal算法证明
对图的顶点数(n)做归纳,证明(Kruskal)算法对任意(n)阶图都适用
归纳基础
(n=1),显然能找到最小生成树
归纳过程
例题
P1550 [USACO08OCT]打井Watering Hole
思路就是建立一个超级源点往每个点建一条边权为(w_i)的边,然后跑最小生成树
怎么证明合数都可以分成素数的乘积
就不写了,我太菜了
在最大生成树上求两点路径中边权的最小值
int f[N][20]; //i的第2^j祖先
int mn[N][20]; //i往上跳2^j祖先所经过的边的最小值
int query(int u,int v) {
int ans=inf;
if(dep[u]>dep[v]) swap(u,v);
for(int i=0,k=dep[v]-dep[u];i<=LG[k];++i)
if(k>>i&1) {
ans=min(ans,mn[v][i]);
v=f[v][i];
}
if(u==v) return ans;
for(int i=LG[dep[u]];i>=0;--i)
if(f[u][i]!=f[v][i]) {
ans=min(ans,min(mn[u][i],mn[v][i]));
u=f[u][i],v=f[v][i];
}
ans=min(ans,min(mn[u][0],mn[v][0]));
return ans;
}
kruskal重构树
最短路
SPFA判负环
queue<int> q;
bool inq[N];
int dis[N], len[N];
bool spfa() { //返回值为true表示有负环 否则没有负环
fill(dis+1,dis+n+1,inf);
dis[1]=0,len[1]=1;
q.push(1);
inq[1]=true;
while(!q.empty()) {
int u=q.front() ; q.pop();
inq[u]=false;
for(int i=0;i<e[u].size();++i) {
int v=e[u][i];
int w=W[u][i];
if(dis[u]+w<dis[v]) {
dis[v]=dis[u]+w;
len[v]=len[u]+1;
if(len[v]>=n) return true;
if(!inq[v]) q.push(v),inq[v]=true;
}
}
}
return false;
}
建图技巧
通过添加虚拟点等手段将问题转化
从而达到减少边数等目的。
骚操作,长见识了
拓扑排序
在一个(DAG)有向无环图) 中,我们将图中的顶点以线性方式进行排序,使得对于任何的顶点 (u) 到 (v) 的有向边 ((u,v)) , 都可以有 (u) 在 (v) 的前面。
//拓扑排序 O(n^2)
int ind[N]; //每个点的入度
int seq[N],cnt; //求出的拓扑序
bool vis[N];
bool toposort() { //有环返回false
while(true) {
if(cnt==n) return true;
int k=0;
for(int i=1;i<=n;++i)
if(!vis[i]&&ind[i]==0) {
k=i; break;
}
if(k==0) return false;
seq[++cnt]=k; vis[k]=true;
for(int i=0;i<e[k].size();++i) {
int u=e[k][i];
--ind[u];
}
}
}
queue优化
(O(n+m))
//拓扑排序 O(n+m)
queue<int> q; //q中维护入度为0的点
int ind[N],seq[N],cnt;
bool toposort() { //有环返回false
for(int i=1;i<=n;++i)
if(!ind[i]) q.push(i);
while(!q.empty()) {
int u=q.front(); q.pop();
seq[++cnt]=u;
for(int i=0;i<e[u].size();++i) {
int v=e[u][i];
--ind[v];
if(ind[v]==0) q.push(v);
}
}
return cnt==n;
}
tarjian
推荐洛谷日报
Tarjan算法运行过程
1.按照深度优先遍历的方式遍历这张图。
2.遍历当前节点所出的所有边。在遍历过程中:
( 1 ) 如果当前边的终点还没有访问过,访问。回溯回来之后比较当前节点的low值和终点的low值。将较小的变为当前节点的low值。(因为遍历到终点时有可能触发了2)
( 2 ) 如果已经访问过,那我们一定走到了一个之前已经走过的点(终点的时间戳一定比当前的小)
3.则比较当前节点的low值和终点的dfn值。将较小的变为当前节点的low值
4,在回溯过程中,对于任意节点u用其出边的终点v的low值来更新节点u的low值。因为节点v能够回溯到的已经在栈中的节点,节点u也一定能够回溯到。因为存在从u到v的直接路径,所以v能够到的节点u也一定能够到。
当一个节点的dfn值和low值相等时,这个节点是一个强联通分量的“根”。压栈,输出。
int dfn[N],low[N],dfsclock;
int s[N],top;
int cnt; //当前联通块的编号
int bl[N]; //bl[i]表示i所在的强联通分量编号
vector<int> scc[N]; //scc[i]中存储编号为i强联通分量中的所有点
void dfs(int u) {
low[u]=dfn[u]=++dfsclock;
s[++top]=u;
for(int i=0;i<e[u].size();++i) {
int v=e[u][i];
if(dfn[v]) low[u]=min(low[u],dfn[v]);
else {
dfs(v);
low[u]=min(low[u],low[v]);
}
}
if(low[u]==dfn[u]) {
++cnt;
int v=s[top];
while(v!=u) {
bl[v]=cnt;
scc[cnt].push_back(v);
v=s[--top];
}
bl[u]=cnt;
scc[cnt].push_back(u);
}
}
缩点
一个强连通分量里的点如果看成是一个大点的话,图就可以变成有向无环图((DAG)),然后递推什么的就方便了
差分约束
以后再写吧,感觉就知道是吧不等式组建了个图...我太弱了
环套树/基环树
我们知道,树是最简单的连通无向图。
树中任意两点之间有且只有一条路径
环套树/基环树:树中加一条边的图。
设加的一条边为((u,v)),那么由之前(u,v)间有一条路径,加了这条边之后这条路径和这条边构成一个环。
这也是形成的唯一一个环
我们可以把环放在图中心,这样看起来环上每个点都是一个树根。
找到环的位置很重要
方法类似(noip2016 信息传递)
基环树找环
找环的步骤:
1.随意找一个点开始当成树进行(dfs),并记录每个结点访问的时间戳(dfn)
2.(dfs)的过程中一定会有一个点往(dfn)比自己小的点连了边,那么这条边可以看成加上的那条。记录下这条边((u,v))
3.暴力让(u)和(v)往上爬到根,记录他们分别经过的点。
4.深度最大的他们都经过的点为他们的(lca),(u->lca)之间的所有点(+v->lca)之间的所有点即构成环。
//求基环树中的环 方法一
int fa[N]; //f[i]为i在搜索树中的父亲结点
bool in[N],vis[N]; //表示i是否在环中
int cir[N],cnt;
int x,y;
void dfs(int u,int f) { //f为u的父亲结点
fa[u]=f; vis[u]=1;
for(int i=0 ;i<e[u].size();++i) {
int v=e[u][i];
if(v==f) continue;
if(vis[v]) x=u,y=v;
else dfs(v,u);
}
}
void solve() { //x到y一定是返祖边
dfs(1,0);
while(x!=y) {
cir[++cnt]=x;
x=fa[x];
}
cir[++cnt]=y;
}
//方法二
queue<int>q;
int deg[N],n;
void solve()
{
for(int i=1;i<=n;++i) in[i]=1;
for(int i=1;i<=n;++i)
if(deg[i]==1) q.push(i);
while(!q.empty())
{
int u=q.front();
q.pop();
int[u]=0;
for(int i=0;i<e[u].size();++i)
{
int v=e[u][i];
deg[v]--;
if(deg[v]==0) q.push(v);
}
}
int u;//u作为环的起点
for(u=1;u<=n;++u)
if(in[u]) break;
int v=u,las=0;//把u放环里
do{
cir[++cnt]=v;
int k;
for(i=0;i<e[v].size();++i)
{
k=e[v][i];
if(in[k] && k!=las) break;
}
la
v=k;
}while(v!=u)
}
欧拉图
通过图中所有边一次且仅一次行遍所有顶点的通路称为欧拉通路。
通过图中所有边一次且仅一次行遍所有顶点的回路称为欧拉回路。
具有欧拉回路的图称为欧拉图。
具有欧拉通路的图称为半欧拉图。
有向图的时候可以类似地定义。
(G)是欧拉图当且仅当(G)是连通的且没有奇度顶点。
$ G(是半欧拉图当且仅当)G$中恰有两个奇度顶点。
证:
连通且没有奇度顶点=>一定存在一个环
那么我们把这个环上的边删掉,剩下的图仍满足连通且没有奇数顶点
由连通性这些环可以是有公共点的,可以拼起来
圈套圈算法
(dfs)搜索,不能再往下走(不能重复使用一条边,但可以重复经过一个点)便回溯,回溯时记录路径,回溯时不清除对边的标记,最后求出来的路径就是欧拉回路。
给边编号,用(vis)数组记录每个边是否访问过
//欧拉回路 圈套圈算法
struct edge
{
int v,nex,id;
};
bool vis[N];
int s[N],top;
int seq[N],cnt;
void dfs(int u)
{
s[++top]=u;
for(int i=head[u];i;i=e[i].nex)
{
if(vis[e[i].id]) continue;
vis[e[i].id]=true;
dfs(e[i].v);
}
--top;
seq[++top]=e[i].v;
}