tarjan算法 习题
dfs树与tarjan算法
标签(空格分隔): 517coding problem solution dfs树 tarjan
Task 1
给出一幅无向图\(G\),在其中给出一个dfs树\(T\),选出\(G\)中的一个边集\(E\),使得在所有T-Simple-Circle(即最多有1条边不在\(T\)中的路径)包含至少一个\(E\)中的元素,最小化\(E\)中元素个数。
对于100%的数据 \(1 \leq n \leq 2 \times 10^3, 1 \leq m \leq 2\times 10^4\)
考虑到选取边一定是在\(T\)中,所以对于一个简单环构成的路径,连接着当前节点和它的祖先节点这一条树上的路径,这条路径上必须选择一条边被选择。
将边下放到其对应子节点,
考虑到一个贪心,只需要将这些链按照深度较浅的节点深度递减排序,每一次如果当前链没有被点覆盖,那么就将深度较浅的点覆盖,这样线段本身被覆盖然后对后续的线段有不低于放在深度较浅的点之下的点的效果。
虽然可以树链剖分达到\(O(n{ log_2 }^2 n)\)的复杂度,但没有必要。
最终复杂度是\(O(n^2)\)
# include<bits/stdc++.h>
using namespace std;
const int N=2e3+10,M=2e4+10;
struct rec{ int pre,to; }a[M<<1];
struct node{ int u,v; }rec[N];
int n,m,tot=1,g;
int head[N],dep[N],pre[N];
bool mark[N],inTree[M<<1];
vector<node>V;
void clear()
{
tot=1;
memset(a,0,sizeof(a)); memset(rec,0,sizeof(rec));
memset(dep,0,sizeof(dep)); memset(head,0,sizeof(head));
memset(pre,0,sizeof(pre)); memset(mark,false,sizeof(mark));
memset(inTree,false,sizeof(inTree)); V.clear();
}
void adde(int u,int v,bool flag)
{
a[++tot].pre=head[u];
a[tot].to=v;
head[u]=tot;
if (flag) inTree[tot]=1;
}
void dfs(int u,int fa)
{
dep[u]=dep[fa]+1;
for (int i=head[u];i;i=a[i].pre){
int v=a[i].to; if (v==fa) continue;
pre[v]=u; dfs(v,u);
}
}
bool cmp(node x,node y){return dep[x.v]>dep[y.v];}
bool check(int s,int t)
{
while (pre[s]!=t) {
if (mark[s]) return true;
s=pre[s];
}
if (mark[s]) return true;
g=s;
return false;
}
int main()
{
//freopen("data.in","r",stdin);
while (true) {
scanf("%d%d",&n,&m); if (!n&&!m) break;
for (int i=1;i<n;i++) {
int u,v; scanf("%d%d",&u,&v);
adde(u,v,1); adde(v,u,1);
rec[i]=(node){u,v};
}
dfs(1,0);
for (int i=n;i<=m;i++) {
int u,v; scanf("%d%d",&u,&v);
adde(u,v,0); adde(v,u,0);
}
for (int i=1;i<n;i++) {
int u=rec[i].u;
for (int w=head[u];w;w=a[w].pre){
int v=a[w].to; if (dep[v]>dep[u]||inTree[w]) continue;
V.push_back((node){u,v});
}
u=rec[i].v;
for (int w=head[u];w;w=a[w].pre){
int v=a[w].to; if (dep[v]>dep[u]||inTree[w]) continue;
V.push_back((node){u,v});
}
}
sort(V.begin(),V.end(),cmp);
int ans=0;
for (int i=0;i<V.size();i++) {
int s=V[i].u,t=V[i].v;
if (check(s,t)) continue;
else mark[g]=1,ans++;
}
printf("%d\n",ans);
clear();
}
return 0;
}
Task 2
在无向图\(G\)中取任意一个生成树,求出这些生成树必然会经过哪些边。
对于100%的数据$2 \leq N \leq 10^4, 1\leq M \leq 10^5 $
如果一条边是割边,那么它必然会出现在生成树中。
本题转化为求出所有的割边。
使用tarjan算法,其中dfn表示无向图的一棵dfs树中节点的dfs序编号,low表示节点经过一条不在dfs树上的边能连接到的节点中dfs序最小值。
显然,割边的判断条件是对于边\((u,v)是割边\),当且仅当$low_{v} > dfn_{u} $
如有重边,仍然可以处理,显然只需要记一下当前进入该节点的边编号(可以用成对变换)如果当前回到父亲节点的这条边不是来的那条边,就说明这两个节点直接含有重边了,一旦含有重边,那么子节点的low值就可以用父亲的dfs序值更新了。
这道题就做完了,\(O(n)\)
# include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=1e5+10;
struct rec{ int pre,to; }a[M<<1];
int dfn[N],low[N],n,m,head[N],tot=1;
bool bridge[M<<1];
void clear()
{
tot=1;
memset(a,0,sizeof(a)); memset(dfn,0,sizeof(dfn));
memset(low,0,sizeof(low)); memset(head,0,sizeof(head));
memset(bridge,false,sizeof(bridge));
}
void adde(int u,int v)
{
a[++tot].pre=head[u];
a[tot].to=v;
head[u]=tot;
}
void tarjan(int u,int id)
{
low[u]=dfn[u]=++dfn[0];
for (int i=head[u];i;i=a[i].pre) {
int v=a[i].to;
if (!dfn[v]) {
tarjan(v,i); low[u]=min(low[u],low[v]);
if (low[v]>dfn[u]) bridge[i]=bridge[i^1]=1;
} else if (i!=(id^1)) low[u]=min(low[u],dfn[v]);
}
}
int main()
{
int T; scanf("%d",&T);
while (T--) {
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++) {
int u,v; scanf("%d%d",&u,&v);
adde(u,v); adde(v,u);
}
tarjan(1,0); int cnt=0;
for (int i=2;i<=tot;i+=2) if (bridge[i]) cnt++;
printf("%d\n",cnt);
for (int i=2;i<=tot;i+=2)
if (bridge[i]) printf("%d%c",i>>1,(--cnt)==0?'\n':' ');
if (T!=0) puts("");
clear();
}
return 0;
}
Task 3
无向图找割点
我也不知道它的输入为什么这么复杂(是为了掩饰这道题水的本质吗?)
# include <iostream>
# include <cstdio>
# include <cstring>
# define min(a,b) ((a)<(b)?(a):(b))
# define max(a,b) ((a)<(b)?(b):(a))
using namespace std;
const int N=105,M=N*N;
struct rec{ int pre,to; }a[M<<1];
bool cur[N];
int dfn[N],low[N],head[N],root;
int n,m,tot;
void clear()
{
memset(a,0,sizeof(a)); memset(cur,false,sizeof(cur));
memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low));
memset(head,0,sizeof(head)); tot=0;
}
void adde(int u,int v)
{
a[++tot].pre=head[u];
a[tot].to=v;
head[u]=tot;
}
void tarjan(int u)
{
dfn[u]=low[u]=++dfn[0];
int flag=0;
for (int i=head[u];i;i=a[i].pre) {
int v=a[i].to;
if (!dfn[v]){
tarjan(v); low[u]=min(low[u],low[v]);
if (low[v]>=dfn[u]) {
flag++;
if (u!=root || flag>1) cur[u]=1;
}
} else low[u]=min(low[u],dfn[v]);
}
}
int main()
{
while (true) {
scanf("%d",&n); if (!n) break;
while (true) {
int u; scanf("%d",&u); if (!u) break;
while (getchar()!='\n') {
int v; scanf("%d",&v);
adde(u,v); adde(v,u);
}
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(root=i);
int ans=0;
for (int i=1;i<=n;i++) if (cur[i]) ans++;
printf("%d\n",ans);
clear();
}
return 0;
}
Task 4
有向图\(G\)中各节点都有一个权值,从\(s\)点出发到\(P\)个合法终点中的一个被认为是一条合法路径。选取一条路径,可以重复经过一些点(但是其权值只可以获得一次),最大化经过路径权值和。
对于100%的数据$1 \leq P\leq n,m\leq 5 \times 10^5 $
考虑到如果有环的话,那么最优的走法就是绕一圈环获取所有权值。
于是我们采用了\(tarjan\)算法来缩点,然后问题就转化为在\(DAG\)`上从\(s\)点出发到任意点的最长路了。(环中节点答案是一样的)
可以使用dp做法,将\(DAG\)进行拓扑排序,按照拓扑序做动态规划。
如果某个缩点的拓扑序在\(s\)所在的拓扑序之前,那么\(s\)是不可能走到那里的。
设\(f_u\)表示从\(s\)点到\(u\)点的最长路,转移就是\(f_u=max\{f_{from} + \sum w_u\}\)
答案就是包含合法终点的缩点的最大值。
还要注意一些问题,比如说dp初值除了出发点,其他必须全部设为-inf
由于dp的时候是取max的,所以重边应该没什么问题。
# include <iostream>
# include <cstdio>
# include <cstring>
# include <vector>
# include <stack>
# include <queue>
# include <algorithm>
# define int long long
using namespace std;
const int N=5e5+10;
struct rec{ int pre,to; }a[N<<1];
stack<int>s; queue<int>q;
bool ok[N],ins[N];
int dfn[N],low[N],c[N],val[N],w[N],head[N],seq[N],in[N],f[N];
vector<int>E[N],F[N];
vector<int>scc[N];
vector<pair<int,int> >Edge;
int n,m,tot,cnt;
void adde(int u,int v)
{
a[++tot].pre=head[u];
a[tot].to=v;
head[u]=tot;
}
void tarjan(int u)
{
dfn[u]=low[u]=++dfn[0];s.push(u);ins[u]=1;
for (int i=head[u];i;i=a[i].pre){
int v=a[i].to;
if (!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
else if (ins[v]) low[u]=min(low[u],dfn[v]);
}
if (dfn[u]==low[u]) {
int v; ++cnt;
do {
v=s.top(); s.pop(); ins[v]=0;
c[v]=cnt; scc[cnt].push_back(v);
val[cnt]+=w[v];
} while (v!=u);
}
}
void toposort(int s)
{
if (!in[s]) q.push(s);
for (int i=1;i<=cnt;i++) if (!in[i] && i!=s) q.push(i);
while (!q.empty()) {
int u=q.front();q.pop();
seq[++seq[0]]=u;
for (int i=0;i<E[u].size();i++) {
int v=E[u][i];
in[v]--; if (!in[v]) q.push(v);
}
}
}
signed main()
{
scanf("%lld%lld",&n,&m);
for (int i=1;i<=m;i++) {
int u,v; scanf("%lld%lld",&u,&v);
adde(u,v);
}
for (int i=1;i<=n;i++) scanf("%lld",&w[i]);
int s,p; scanf("%lld%lld",&s,&p);
for (int i=1;i<=p;i++) { int t; scanf("%lld",&t); ok[t]=1;}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
for (int u=1;u<=n;u++)
for (int i=head[u];i;i=a[i].pre) {
int v=a[i].to; if (c[u]==c[v]) continue;
Edge.push_back(make_pair(c[u],c[v]));
}
sort(Edge.begin(),Edge.end());
Edge.erase((unique(Edge.begin(),Edge.end())),Edge.end());
for (int i=0;i<Edge.size();i++) {
int u=Edge[i].first,v=Edge[i].second;
E[u].push_back(v); in[v]++;
F[v].push_back(u);
}
toposort(s);
int pt; for (int i=1;i<=cnt;i++) if (c[s]==seq[i]) { pt=i; break;}
memset(f,-0x3f,sizeof(f)); f[c[s]]=val[c[s]];
for (int w=pt+1;w<=cnt;w++) {
int u=seq[w];
for (int i=0;i<F[u].size();i++) {
int v=F[u][i];
f[u]=max(f[u],f[v]);
}
f[u]+=val[u];
}
int ans=0;
for (int i=1;i<=n;i++) if (ok[i]) ans=max(ans,f[c[i]]);
printf("%lld\n",ans);
return 0;
}
Task 5
有向图\(G\)中至少需加多少条有向边才能使每个节点都能从节点\(s\)出发被访问到。
对于100%的数据 \(1\leq s \leq n,m \leq 5000\)
缩点以后,图变成了\(DAG\)上的问题。
做一遍$ dfs \(找出所有不能和\)s\(所在的点联通的块,对于每一个连通块至少需要一条边才能和\)s$所在的点连通。
考虑到每一个入度为0的连通块其必然会加一条边使得其连接到\(s\)所在的连通块或者其他连通块。
所以答案是 不在\(s\)缩点里的且入度为0的连通块个数。
复杂度\(O(n)\)
# include <bits/stdc++.h>
using namespace std;
const int N=5005;
struct rec{ int pre,to; }a[N<<1];
stack<int>s;
int low[N],dfn[N],head[N],c[N],in[N];
bool ins[N],vis[N];
int n,m,tot,cnt;
vector<int>E[N];
void adde(int u,int v)
{
a[++tot].pre=head[u];
a[tot].to=v;
head[u]=tot;
}
void tarjan(int u)
{
dfn[u]=low[u]=++dfn[0]; s.push(u); ins[u]=1;
for (int i=head[u];i;i=a[i].pre){
int v=a[i].to;
if (!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
else if (ins[v]) low[u]=min(low[u],dfn[v]);
}
if (low[u]==dfn[u]) {
int v; ++cnt;
do {
v=s.top();s.pop();ins[v]=0;
c[v]=cnt;
} while (v!=u);
}
}
void dfs(int u)
{
vis[u]=1;
for (int i=0;i<E[u].size();i++) {
int v=E[u][i]; if (vis[v]) continue;
dfs(v);
}
}
int main()
{
int s; scanf("%d%d%d",&n,&m,&s);
for (int i=1;i<=m;i++) {
int u,v; scanf("%d%d",&u,&v);
adde(u,v);
}
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
for (int u=1;u<=n;u++)
for (int i=head[u];i;i=a[i].pre) {
int v=a[i].to; if (c[u]==c[v]) continue;
E[c[u]].push_back(c[v]); in[c[v]]++;
}
dfs(c[s]); int ans=0;
for (int i=1;i<=cnt;i++) if (!vis[i] && !in[i] ) ans++;
printf("%d\n",ans);
return 0;
}
tarjan算法 习题的更多相关文章
- 近期公共祖先(LCA)——离线Tarjan算法+并查集优化
一. 离线Tarjan算法 LCA问题(lowest common ancestors):在一个有根树T中.两个节点和 e&sig=3136f1d5fcf75709d9ac882bd8cfe0 ...
- 有向图强连通分量的Tarjan算法
有向图强连通分量的Tarjan算法 [有向图强连通分量] 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G ...
- 点/边 双连通分量---Tarjan算法
运用Tarjan算法,求解图的点/边双连通分量. 1.点双连通分量[块] 割点可以存在多个块中,每个块包含当前节点u,分量以边的形式输出比较有意义. typedef struct{ //栈结点结构 保 ...
- 割点和桥---Tarjan算法
使用Tarjan算法求解图的割点和桥. 1.割点 主要的算法结构就是DFS,一个点是割点,当且仅当以下两种情况: (1)该节点是根节点,且有两棵以上的子树; (2)该节 ...
- Tarjan算法---强联通分量
1.基础知识 在有向图G,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极大强连通子 ...
- (转载)LCA问题的Tarjan算法
转载自:Click Here LCA问题(Lowest Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u, v)(通常查询数量较大),每次求树T中两 ...
- 强连通分量的Tarjan算法
资料参考 Tarjan算法寻找有向图的强连通分量 基于强联通的tarjan算法详解 有向图强连通分量的Tarjan算法 处理SCC(强连通分量问题)的Tarjan算法 强连通分量的三种算法分析 Tar ...
- [知识点]Tarjan算法
// 此博文为迁移而来,写于2015年4月14日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vxnx.html UPD ...
- Tarjan 算法&模板
Tarjan 算法 一.算法简介 Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度. 我们定义: 如果两个顶点可以相互通达,则称两个顶点强连 ...
随机推荐
- python之cookies
#cookies保存在文档头的内部,将cookies信息保存在文档中 userinfo={'} r=requests.get('http://httpbin.org/get',cookies=user ...
- kafka整理笔记笔记
一.为什么需要消息系统 解耦: 允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束. 冗余: 消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险.许多消息 ...
- Git 实习一个月恍然大悟合集
从开始实习到现在大概有一个月了,这个月时间接触了很多新东西,其中就包括了git版本控制.分支管理等等.我在这段时间里,深深地感受到了git对公司项目代码管理和控制.团队合作带来的益处和其重要性.其实在 ...
- pycharm 快捷键练习 和基本英语单词练习
通过练习 一下快捷键 打代码的速度得到提升 pycharm以下 快捷键+快捷键意义 ctrl+a 全选 ctrl+c 复制(不选中默认复制一行) ctrl+v 粘贴 ctrl+x 剪切 ctrl+f ...
- java native本地方法详解(转)
文章链接出处: 详解native方法的使用 自己实现一个Native方法的调用 JNI 开始本篇的内容之前,首先要讲一下JNI.Java很好,使用的人很多.应用极 广,但是Java不是完美的.Java ...
- [Nest] 02.nest之控制器
控制器 Controller Nest 的核心概念 模块 Module 控制器 Controller 服务与依赖注入 Provider Dependency injection 控制器负责处理应用的特 ...
- Git复习(四)之解决冲突
解决冲突 合并分支往往也不是一帆风顺的 假设:我们从master创建了一个新的分支feature1更改了最后一行提交,我们切换到master分支也更改了最后一行提交,现在,master分支和featu ...
- js 禁用F12 和右键查看源码
<script> window.onkeydown = function(e) { if (e.keyCode === 123) { e.preventDefault() } } wind ...
- 北上广Java开发月薪20K往上,该如何做,需要会写什么
这个问题可能很多人会说这只是大企业或者互联网企业工程师才能拿到.也许是的,小公司或者非互联网企业拿两万的不太可能是码农了,应该已经转管理.还有区域问题,这个不在我的考虑范围内,因为这方面除了北上广深杭 ...
- CXF实现webService服务(一)
以前工作中也用CXF,但都是用别人现成搭好的环境,这次自己重头搭建一遍环境.过程中也有遇到的问题,也做了简单的整理. 对于CXF是干什么用的,我不想多说,大家都知道这是我们在Java编程中webSer ...