【学短向】图的存储与遍历,最短路,连通性 tarjan,树状数组
树状数组
单点修改,前缀查询。
int lbd(int x) {return (x&(-x));}
void gai(int x,int c) {for(int i=x;i<=n;i+=lbd(i)) a[i]+=c;}
int cha(int x) {int da=0;for(int i=x;i;i-=lbd(i)) da+=a[i];return da;}
上面这个模板同样是单点加/单点改/单点取max + 查前缀 max/查前缀和。
如果查后缀和只需要把 gai() 和 cha() 的 + 和 - 互换。
查询信息可减就可以直接查区间(两个前缀和减一下)。
优点是代码比线段树短。
什么?你想要更炫酷的序列操作?
哎不是你是不是为难学哥,不久有个讲线段树的学哥你去问他。
图的存储与遍历
链式前向星
const int QAQ=1001,ovo=QAQ*QAQ;
int cnt,head[QAQ],to[ovo],nex[ovo],bq[ovo];
void lian(int u,int v,int w)
{
++cnt;
to[cnt]=v;
nex[cnt]=head[u];
bq[cnt]=w;
head[u]=cnt;
}
void dfs(int x,int dis)
{
for(int i=head[x];i;i=nex[i])
{
int v=to[i],w=bq[i];
dfs(v,dis+w);
}
}
优点:
- 方便地找到反向边:初始化
cnt=1,i^1号边为i号边的反向边。
vector
const int QAQ=1001;
vector<int> dian[QAQ],bian[QAQ];
void lian(int u,int v,int w)
{
dian[u].push_back(v);
bian[u].push_back(w);
}
void dfs(int x,int dis)
{
for(int i=0;i<dian[x].size();i++)
{
int v=dian[x][i],w=bian[x][i];
dfs(v,dis+w);
}
}
最好理解的存图。
最短路
听说大家学过了。
感觉上
dij 像贪心,稳定 \(O(n\log n)\),不能跑负边权。
spfa 像乱搞暴搜,不稳定 \(O(kn)\),\(k\) 是个很小的常数,但是在出题人想卡的情况下会卡到 \(O(n^2)\)。但是不刻意去卡会跑得很快,有负边权的时候肯定不会卡 SPFA。
弗洛伊德 像 DP,稳定 \(O(n^3)\)。
bfs 只能跑 01 边权 \(O(n)\)。(实际上是可以跑整个图只有两种边权的)
【代码】SPFA:
bool vis[110000];
int n,m,s,dis[110000];
vector<int> dian[110000],bian[110000];
void sp(int s)
{
queue<int> dui;
dis[s]=0;
dui.push(s);
vis[s]=1;
while(!dui.empty())
{
int x=dui.front();dui.pop();
vis[x]=0;
int chang=dian[x].size();
for(int i=0;i<chang;i++)
{
int v=dian[x][i];
if(dis[v]>dis[x]+bian[x][i])
{
dis[v]=dis[x]+bian[x][i];
if(!vis[x]) dui.push(v),vis[v]=1;
}
}
}
}
【代码】dij:
vector<int> dian[110000],bian[110000];
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > dui;
void dij(int s)
{
dis[s]=0;
dui.push(make_pair(0,s));
while(!dui.empty())
{
int x=dui.top().second;dui.pop();
if(vis[x]) continue;
vis[x]=1;
int chang=dian[x].size();
for(int i=0;i<chang;i++)
{
int v=dian[x][i];
if(dis[v]>=dis[x]+bian[x][i])
{
dis[v]=dis[x]+bian[x][i];
dui.push(make_pair(dis[v],v));
}
}
}
}
【代码】bfs【代码】弗洛伊德 懒得粘了。
例题:
P1462 通往奥格瑞玛的道路
P1629 邮递员送信
P3385 【模板】负环
P1144 最短路计数
P9377 [THUPC 2023 决赛] 百合
P4001 [ICPC-Beijing 2006] 狼抓兔子
连通性 tarjan
这里的连通性并非并查集之类的,把那些东西收起来!
强连通:有向图中任意两个点连通(互相走到)。
强连通分量:极大的强连通子图。(理解一下极大:能括就括,强连通分量的子图不是强连通分量)
找强连通分量
(注意别忘了我们在有向图)
因为强连通分量任意两个点能互相走到,所以我们直接从一个点开始 dfs,可以构成一个 dfs 树:
返祖边:字面意思,如上图红边。
\(dfn_i\):\(i\) 的 dfs 序。
\(low_i\):在 dfs 树上,\(i\) 的子树内的点经过至多一条返祖边可以到达的最小 dfs 序。
手玩一下可以发现 \(dfn_i=low_i\) 的时候 \(i\) 为一个强连通分量的根,这时候子树内和 \(i\) 直接连通的 \(dfn_j<low_j\) 的这些点一起组成当前的强连通分量。
那么 \(dfn_j<low_j\) 如何提出来呢?
你发现父亲的 \(low\) 肯定比儿子的 \(low\) 小,宏观上看由下到上 \((dfn_j>low_j)→(dfn_j=low_j)→(dfn_j<low_j)\)。
然后我们发现在 \(dfn_j=low_j\) 处会把前面的 \(dfn_j>low_j\) 统计并弹掉。
从下往上第一个强连通分量肯定是把子树都提出来并弹掉。
假设他有一个祖先为根的第二个强连通分量,这个强连通分量也是把当前没被弹走子树的点提出来弹掉。
你会发现用一个栈维护就 ok 了。
其实这个栈里的 dfs 序也是单调递增的,因为 dfs 本身就是个栈(先进先出)搜索。
栈具体的维护:每次 \(dfn_i=low_i\) 的时候开始弹栈弹到栈顶为 \(i\),然后再把栈顶也弹出来,这一轮弹出来的就是个强连通分量,至于为什么不是强连通分量的子图,大家可以手玩几个图理解(你想到的那些跑出强连通子图的情况实际上都不符合 dfs 树与 tarjan 其他操作的过程)。
void tajian(int x)
{
dfn[x]=++hao,low[x]=dfn[x];
zai[x]=1,zhan[++top]=x;
// zai 数组是是否在当前点到根路径上,用来判断是否是返祖边。
// zhan 是栈。
for(int i=head[x];i;i=e[i].nex)
{
int v=e[i].to;
if(!dfn[v]) tajian(v),low[x]=min(low[x],low[v]);
//第一种:遇到 dfs 树上的儿子了。
else if(zai[v]) low[x]=min(low[x],dfn[v]);
//第二种:遇到返祖边了。
}
if(dfn[x]==low[x])//我是强连通分量的根。
{
++ke;//ke 是当前强连通分量的编号。
while(zhan[top]!=x) kuai[zhan[top]]=ke,zai[zhan[top]]=0,top--;
//把当前点为根的强连通分量提出来弹掉。
kuai[zhan[top]]=ke,zai[zhan[top]]=0,top--;
}
}
【模板】缩点。
sol:
发现走到强连通分量把整个分量走一遍是最好的,所以缩点,然后剩下一个 DAG,这个显然可以按拓扑序跑个 DP。
\(f_x\):走到 \(x\) 节点的最大权值和。
\(f_v=\max(f_v,f_x+w)\)。
注意下面是无向图
点双连通分量
点双连通分量:一个点删掉后仍然连通的极大子图。
上面的这个点称为割点。
找割点
如果存在 \(v\) 使得 \(low_v\ge dfn_x\),则 \(x\) 为割点
注意特判根节点如果有大于一个儿子则为割点。
std:
void tajian(int u)
{
int ji=0;
dfn[u]=++cnt;low[u]=cnt;
for(int i=head[u],v;i;i=xia[i])
{
if(vis[i^1]) continue; vis[i]=1;
v=to[i];
if(!dfn[v])
{
tajian(v),low[u]=min(low[u],low[v]);
if(u!=gen&&low[v]>=dfn[u]) ok[u]=1;
ji++;
}
else low[u]=min(low[u],dfn[v]);
}
if(u==gen&&ji>1) ok[u]=1;
}
找点双连通分量
割点是点双的根,在割点退栈即可。
std:
void tajian(int u)
{
int son=0;
dfn[u]=++cnt;low[u]=cnt;
zhan[++top]=u;
for(int i=head[u],v;i;i=xia[i])
{
if(vis[i^1]) continue; vis[i]=1;
v=to[i];
if(!dfn[v])
{
son++,tajian(v),low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])
{
std::vector<int> x;
x.push_back(u);
while(zhan[top]!=v) x.push_back(zhan[top--]);
x.push_back(zhan[top--]);
ans.push_back(x);
}
}
else low[u]=min(low[u],dfn[v]);
}
if(gen==u&&!son)
{
vector<int> x;
x.push_back(u);
ans.push_back(x);
}
}
边双连通分量
边双连通分量:一个边删掉后仍然连通的极大子图。
上面的这个边称为割边。
求边双连通分量
如果 \(low_x=dfn_x\),则 \(x\) 为割点
注意特判根节点如果有大于一个儿子则为割点。
std:
void tajian(int u)
{
dfn[u]=++cnt;low[u]=cnt;
zhan[++top]=u;
for(int i=head[u],v;i;i=xia[i])
{
if(vis[i^1]) continue; vis[i]=1;
v=to[i];
if(!dfn[v]) tajian(v),low[u]=min(low[u],low[v]);
else low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
vector<int> x=kong;
while(zhan[top]!=u&&top) x.push_back(zhan[top--]);
x.push_back(zhan[top--]);
ans.push_back(x);
}
}
求割边
std:
void tajian(int u)
{
++hao,dfn[u]=hao,low[u]=hao;
for(int i=head[u],v;i;i=xia[i])
{
if(vis[i^1]) continue;
vis[i]=1;
v=to[i];
if(!dfn[v])
{
tajian(v),low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) ok[i]=ok[i^1]=1;
}
else low[u]=min(low[u],dfn[v]);
}
}
例题:

浙公网安备 33010602011771号