《算法竞赛进阶指南》------图论篇3

0x14 岛屿(基环树直径+拓扑排序+树的直径)

岛屿
题意:n个岛屿,n座桥。问基环森立直径之和。
思路:用BFS遍历,找出每一个基环树,用拓扑排序求出环上的子树的直径,和环上点的集合,顺便得到该环的点为根的树,以根为起点的最长链。再用双指针和优先队列求基环树直径。
ACcode详解:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e6+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ ll to,next,val;} Edge[N<<1];
int tot=1,head[N],deg[N];
int vis[N],vis1[N];  //标记
vector<int> v,huan;
ll a[N<<1],sum[N<<1];   // a:以第i号环点为起点的最长链 sum:环边的权值
ll d[N],f[N]; // 子树最长链 好直径
void AddEdge(int u,int v,ll val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
void bfs(int x){ //找到基环树并将点存放在v中
    v.clear(); // 清空
    queue<int> q;
    q.push(x);
    vis[x] =1;
    while(q.size()){
        int x = q.front();
        v.push_back(x);
        q.pop();
        for(int i=head[x];i;i=Edge[i].next){
            int y = Edge[i].to;
            if(vis[y]) continue;
            vis[y] = 1;
            q.push(y);
        }
    }
    
}
void topsort(){  // // 将环的子树求出直径并给环的顶点 ,和环上点的v。 将环的点存放在huan容器中
    // 拓扑排序
    queue<int> q;
    for(int i=0;i<(int)v.size();i++){
        if(deg[v[i]]==1)  {
        q.push(v[i]);
        }
    }
    // for(int i=0;i<v.size();i++) cout<<v[i]<<" "; cout<<endl;
    while(q.size()){
        int x = q.front();
        q.pop();
        vis1[x] = 1;
        for(int i=head[x];i;i=Edge[i].next){
            int y = Edge[i].to;
            if(vis1[y]) continue;
            f[y] = max(f[y],f[x]);  //纯纯的上递给根节点,环上的点
            f[y] = max(f[y],d[y]+d[x]+Edge[i].val);
            // 更新d[y] // 给最大的给你
            d[y] =max(d[y],d[x]+Edge[i].val);
            // 根据拓扑排序,x点没了, x指向的y度--
            deg[y]--;
            if(deg[y]==1) q.push(y);  // 环上的点不可能没有,所以剩下的点时环上的点
        }
    }
    huan.clear();// 清空
    // 环上的点
    for(int i=0;i<(int)v.size();i++){
        if(!vis1[v[i]]) {
            huan.push_back(v[i]);
        }
    }
}
int q[N<<1];
ll cal(){  // 该干嘛呢?  将环上的点d和f映射到a, 经过环与不过环。
    ll res = 0;
    int u = huan[0];
    int cnt=0;
    int last = 0;
    do{
        for(int i=head[u];i;i=Edge[i].next){
            int v = Edge[i].to;
            if(last == i) continue;
            if(vis1[v]) continue;
            last = i^1;
            vis1[v] = 1;
            a[cnt] = d[v];
            u = v;
            sum[cnt++] = Edge[i].val; // 环上的边
            res = max(res,f[v]);
            break; // 一次就好
        }
    }while(u!=huan[0]); 
    int num = huan.size();
    memcpy(a+num,a,num*sizeof(ll));  //环上,数组拉两倍
    memcpy(sum+num,sum,num*sizeof(ll));
    int l,r;
    l=0,r=-1;  
    for(int k=0;k<(num<<1);k++){ 
        // 求和
        if(k) sum[k] +=sum[k-1]; 
        if(l<=r &&  k -q[l] >=num){  // 数组拉长两倍,但过环上的点个数不能超过num
            l++;  
        }
        if(l<=r){
            int j = q[l];
            res = max(res,a[j] + a[k] + sum[k]-sum[j]);
            // cout<<a[j] + a[k] + sum[k]-sum[j]<<" "<<a[j]<<" "<<a[k]<<" "<<j<<" "<<k<<endl;
        }
        while(l<=r && a[k]-a[q[r]] >= sum[k]-sum[q[r]]) r--;   //单调队列 从大到小 
        q[++r] = k;
    }
    return res;
}
void solve(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,y;
        ll val;
        x = i;
        cin>> y >> val;
        AddEdge(x,y,val);
        AddEdge(y,x,val);
        deg[x]++;
        deg[y]++;
    }
    ll res=0;
    // 基环森林 用 vis标记
    for(int i=1;i<=n;i++){
        if(vis[i]) continue;
        bfs(i);   // 找到基环树并将点存放在v中,并各个点vis标记为1
        topsort(); // 将环的子树求出直径并给环的顶点 ,和环上点的v。 将环的点存放在huan容器中
        res+=cal();    //基环树最长的链
    }
    cout<<res<<endl;
    return ;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x15 创世纪(基环树+找环上的一点+两次树上dp,删边)

创世纪
题意:n种元素,第i号向它的元素 A [ i ] A[i] A[i]进行限制(人话:一条i指向A[i]的边),现在要将一些元素拿去建造世界,但被拿去元素的有个要求:在未被拿去的元素中至少有一个能限制拿去的元素。
问最多可以投放多少元素去建造世界。
思路: 画出图,是基环树的内向树,那不妨所有边方向反向,环还是环,环上的链是树。
树上 d p : F [ i ] [ 2 ] dp:F[i][2] dp:F[i][2],N号结点,0表示i号未被拿走,1表示已被拿去。
用A[i]数组的儿子套父亲,父亲套儿子,找到环上的一点,之后屏蔽root到A[root]的边树上DP一次。再是root对A[root]有限制,进行一次树上DP。两次比较得到最多的。基环森林求和得到结果。
ACcode详解:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e6+10;
const ll P=1e9+7;
const int INF = 1e6+10;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ int to,next;} Edge[N];
int tot,head[N];
void AddEdge(int u,int v){
    Edge[++tot] = (E){v,head[u]};
    head[u]  =tot;
}
int A[N];
int vis[N];
int F[N][2]; // 点 不去与去
int flag;
int root;
void dfs(int u){
    vis[u] = 1;
    int kt=0;
    int dif=INF;
    int num=0;
    int sum =0;
    for(int i=head[u];i;i=Edge[i].next){
        int v = Edge[i].to;
        if(v==root) continue;
        num++;
        dfs(v);  
         // 这里别写 F[u][0] += max(F[v][0],F[v][1]); 因为对树是两次DP,所以在第二次求 F[u][0]他是有初始值。。。。
        sum +=max(F[v][0],F[v][1]);  
        if(F[v][0] > F[v][1]) kt=1;  // 说明u的子节点有未标记的
        else  dif = min(dif,F[v][1]-F[v][0]);
    }
    F[u][0] = sum;    
    if(num==0) { // 叶子结点
        if(flag==1 && u==A[root]) F[u][1] = F[u][0] +1;
        else F[u][1] = 0;
        return ;
    }

    if(kt) F[u][1] = F[u][0]+1;     
    else F[u][1] = F[u][0] +1 - dif;  // 子节点全都要。咳咳,全是1。
    // 最特殊的一种情况 保留root 对 A[root]的限制 
    if(flag==1 && u==A[root]) F[u][1] = F[u][0] +1; //u直接随意就好
}
void solve(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int v;   // i-->v
        cin>>v;
        AddEdge(v,i); // 反着建边
        A[i] = v;
    }
    int ans=0; // 结果
    for(int i=1;i<=n;i++){
        if(vis[i]) continue;
         root=i; // 找根
        while(!vis[root]){
            vis[root] = 1;
            root = A[root];
        }
        // 移除 A[root] -->root 边
        flag =0;
        int res;
        dfs(root);
        res = max(F[root][0],F[root][1]);
        flag = 1;
        dfs(root);
        res = max(res,F[root][0]);   //这里写F[root][1]对A[root]没有啥影响,懂了这一点就出来了。
        ans+=res;
    }
    cout<<ans<<endl;
    return ;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x16 Sightseeing Cows (01规划 + 负环判断)

Sightseeing Cows
题意: n点,m边的有向图 f [ i ] f[i] f[i]为i号权, t [ i ] t[i] t[i] 为第i条边的权值。
问图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大?(题目保证至少存在一个环)
思路:什么之和除以什么之和,一看就要二分,便是01规划咯。 ∑ i = 1 t f [ i ] ∑ i = 1 t t [ i ] > = m i d \frac{\sum_{i=1}^t f[i]}{\sum_{i=1}^t t[i]}>=mid i=1tt[i]i=1tf[i]>=mid ,意味着这次二分的mid 未到达最大值,还能再大点。
将式子变形: ∑ t [ i ] ∗ m i d − f [ i ] < = 0 {\sum t[i]*mid-f[i]}<=0 t[i]midf[i]<=0 ,啊~熟悉的01规划,边权值用 w ( u , v ) = t [ i ] ∗ m i d − f [ i ] w(u,v) = t[i]*mid-f[i] w(u,v)=t[i]midf[i]表示,
然后环是负值,用SPFA算法对图搜寻,cnt数组标记起点到i号店的边数,如果cnt[i]>=n说明啥呢走到环了撒。
ACcode:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=1e4+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ ll to,next,val;} Edge[N];
int tot,head[N];
void AddEdge(int u,int v,int val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
ll f[N],t[N]; // 点权和边权
int cnt[N];  //  路径次数
double dis[N];
int vis[N];
int n,m;
queue<int> q;
bool check(double mid){
    while(q.size()) q.pop();
    for(int i=1;i<=n;i++){
        vis[i] = true;
         //因为不知道起点是哪个开始跑,那就所有的点入队列,我们关心是cnt数组,并不关心dis值的意义,所以不会影响到结果。
        q.push(i);  
        dis[i] = 0;
        cnt[i] = 0;
    }
    int u ,v;
    double w;
    while(q.size()){
         u = q.front();
         vis[u] = 0;  //释放
         q.pop();
         for(int i=head[u];i;i=Edge[i].next){
             v = Edge[i].to;
             w = mid*Edge[i].val - f[u];
             if(dis[u] + w < dis[v]){
                 dis[v] = dis[u] + w;
                 cnt[v] = cnt[u] +1;   // 边的次数
                 if(cnt[v]>=n) return true;
                 if(!vis[v]) q.push(v),vis[v] = 1;
             }
         }
    }
    return false;
}
void solve(){
    
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>f[i];
    for(int i=1;i<=m;i++){
        int u,v,val;
        cin>>u>>v>>val;
        AddEdge(u,v,val);
    }
    double l = 0,r= 1010;
    double eps = 1e-4;
    while(fabs(l-r) > eps){
        double mid = (l+r)/2;
        if(check(mid) ) l = mid; //  还可以更大
        else r = mid;
    }
    printf("%.2lf\n",l);
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x17 Intervals POJ1201 (差分约束+SPFA最长路)

Intervals
题意:n个闭合区间 [ a i , b i ] [a_i,b_i] [ai,bi],使每个区间 [ a i , b i ] [a_i,b_i] [ai,bi] 内都有至少 c i c_i ci个数。

前置知识:差分约束,在前面我们学习单源点最短路径问题中的三角不等式 d i s t [ y ] < = d i s t [ x ] + z dist[y]<=dist[x]+z dist[y]<=dist[x]+z,(x到y上权值为z),即是求最短路的必要条件,变形 X i − X j < = z X_i-X_j<=z XiXj<=z,就成了两点的差值要小于z。
但是源点是哪个呢? 这个时候我就再增加一个0号节点作为源点。 加上之后,就会多出一些不等式 X i − X 0 < = 0 X_i - X_0<=0 XiX0<=0
在这里插入图片描述

求最大值:而往往这样的题往往是要我们求解 X n − X 0 X_n-X_0 XnX0最大值,也就是满足所得小于等与的不等式,那么就是求单源点0起点,到n的最短路长度即是结果了撒,前面说了是最短路的必要条件
就是说 X n − X 0 < = d i s [ X n ] X_n-X_0<=dis[X_n] XnX0<=dis[Xn],那最大值就是 d i s [ X n ] dis[X_n] dis[Xn]咯.( d i s [ X i ] dis[X_i] dis[Xi],i结点最短路长度)
求最小值:那我想求解 X n − X 0 X_n-X_0 XnX0最小值呢?
这个时候我们需将上述的小于等于的不等式两边乘以-1,转成大于等于的不等式,这个时候所有不等式是是什么含义是某点的最长路,即是某点最长路的必要条件是大于等于不等式都要满足,是吧。细细品味。
那是不是说 X n − X 0 > = d i s [ x n ] X_n-X_0>=dis[x_n] XnX0>=dis[xn] ( d i s [ X i ] dis[X_i] dis[Xi],i结点最长路的长度)

扩展:很多时候差分约束的条件并不是简单的小于等于号,这时候我们需要稍微做点变形。
如果有 x i − x j > = c k x_i-x_j>=c_k xixj>=ck,则可以两边同时乘 −1,将不等号反转过来。
如果有 x i − X j = c k x_i-X_j=c_k xiXj=ck,则可以把这个等式拆分为 x i − x j < = c k x_i-x_j<=c_k xixj<=ck x i − x j > = c k x_i-x_j>=c_k xixj>=ck两个约束条件

思路:设 s [ k ] s[k] s[k]表示 0 − k 0-k 0k之间最少选出多少个整数。
依据题意就能得到以下的不等式:
s [ b i ] − s [ a i − 1 ] > = c i s[b_i] - s[a_i-1] >=c_i s[bi]s[ai1]>=ci
一些隐含的条件: 0 < = s [ k + 1 ] − s [ k ] < = 1 0<=s[k+1]-s[k]<=1 0<=s[k+1]s[k]<=1得到:
s [ i ] − s [ i + 1 ] > = − 1 s[i] - s[i+1] >=-1 s[i]s[i+1]>=1
s [ i + 1 ] − s [ i ] > = 0 s[i+1]-s[i]>=0 s[i+1]s[i]>=0
那么这里为什么都用大于等于,因为求出最小选出的整数,即是s[50001]的节点最长路?
为啥是最长路呢?细想一下是不是只有最长路才能满足上述的所有的不等式条件

ACcode:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=3e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ int to,next,val;} Edge[N];
int tot,head[N];
void AddEdge(int u,int v,int val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
int m;
int n;
int ans,dis[N],vis[N];
void SPFA(){   // 求最长路
    queue<int> q;
    q.push(0);
    // 初始化
    memset(dis,-0x3f,sizeof(dis));
    dis[0] = 0;
    while(q.size()){
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(int i=head[u];i;i=Edge[i].next){
            int v = Edge[i].to;
            int distance = dis[u] + Edge[i].val;
            if(distance > dis[v]){
                dis[v]  = distance;
                if(!vis[v]) vis[v] = 1,q.push(v);
            }
        }
    }
    ans  = dis[n];
    return ;
}
void solve(){
    cin>>m;
    for(int i=1;i<=m;i++){
        // 整体右移一个
        int u,v,val;
        cin>>u>>v>>val;
        u++; v++;
        n = max(u,n);
        n = max(n,v);
        AddEdge(u-1,v,val);
    }
    for(int i=0;i<=n;i++){
        AddEdge(i+1,i,-1);
        AddEdge(i,i+1,0);
    }
    SPFA();
    cout<<ans<<endl;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x18 P5960 【模板】差分约束算法 (差分约束+负环判断)

P5960 【模板】差分约束算法
题意:板子题咯,m个不等式,求是否有解,无解就NO。
负环情况是无解,有解输出期中任意的解。
思路:差分约束+最短路
ACcode:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e4+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
int n,m;
struct E{ ll to,next,val;} Edge[N];
int tot,head[N];
void AddEdge(int u,int v,int val){
    Edge[++tot] = (E){v,head[u],val};
    head[u] = tot;
}
int vis[N],cnt[N];
ll dis[N];
bool SPFA(){
    memset(dis,0x3f,sizeof(dis));
    queue<int> q;
    q.push(0);
    vis[0] = 1;
    dis[0] = 0;
    while(q.size()){
        int u = q.front();
        vis[u] = 0;
        q.pop();
        for(int i=head[u];i;i=Edge[i].next){
            int v = Edge[i].to;
            if(dis[v]> dis[u]+Edge[i].val){
                dis[v] = dis[u] + Edge[i].val;
                cnt[v] = cnt[u] +1;
                // 负环判断
                if(cnt[v]>=n+1) return false;  // 额外添加了0号点
                if(!vis[v]) q.push(v),vis[v] =1;
            }
        }
    }
    return   true; 
}
void solve(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v,val;
        cin>>u>>v>>val;
        AddEdge(v,u,val);
    }
    // 初始一下条件   此解法按照算法进阶指南的思路建立图,所以得到结果全是负数或0.
    for(int i=1;i<=n;i++){
        AddEdge(0,i,0);
    }
    if(SPFA()){
        for(int i=1;i<=n;i++){
            if(i!=1) cout<<" "<<dis[i];
            else cout<<dis[i];
        }
    }else{
        cout<<"NO"<<endl;
    }
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x19 P3388 【模板】割点 (割点)

【模板】割点 (割点)
割点详细解释
由于以前写过关于割点的博客,就不在此炒冷饭了。
题意:求割点
思路:注意一些小小细节, dfn和low的关系,以及根节点特判, 给的图不是连通图之类的。
用到的一些dfn和low

dfn[x] = low[x] = ++num;
low[x] = min(low[x],low[y]);  // 搜索树儿子结果会影响到父亲
if(low[y] >= dfn[x]) ;  // 这里要有等号,多想想他是怎么找到的,等号说明x,y刚好是一个环的起点和终点
low[x] = min(dfn[y],low[x]);

ACcode详解: (一年前绞尽脑汁想了好几天才理解一点的割点,现在再次系统刷图论,也不是抽象,难蛮。点滴的记录

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=2e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
ll n,m;
struct E{ int to,next;} Edge[N<<1];
int tot=1,dfn[N],low[N]; //  (2,2^1) (4,4^1)时间戳 时间戳的最小值
int head[N];
void AddEdge(int u,int v){   // 无向图
    Edge[++tot] = E{v,head[u]};
    head[u] = tot;
}
int cut[N];
int root,num;
void tarjan(int x){   // 割点不用考虑重边的情况
    dfn[x] = low[x] = ++num;
    int flag = 0;  //标记 由于根节点的特殊
    for(int i=head[x];i;i=Edge[i].next){
        int y = Edge[i].to;
        // 未访问过:是搜索树
        if(!dfn[y]){
            // 深度搜索
            tarjan(y);
            low[x] = min(low[x],low[y]);  // 搜索树儿子结果会影响到父亲
            // 判断x结点是不是割点: 分为根节点 和非根节点
            if(low[y] >= dfn[x]) {   // 这里要有等号,多想想他是怎么找到的,等号说明x,y刚好是一个环的起点和终点
                flag++;
                // 非根    和   根节点至少有两个这样的
                if(x!=root || flag>1 ) cut[x] = 1; 
            }
        }else{
            // 访问到搜索树已经标记的点了, 说明 x到 y 能形成环了
            low[x] = min(dfn[y],low[x]);
        }
    }
}
void solve(){
    // 找割点
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        AddEdge(u,v);
        AddEdge(v,u);
    }
    for(int i=1;i<=n;i++){
       if(!dfn[i]){       
           root = i;
           tarjan(i);
       }
    }
    int nums = 0;
    for(int i=1;i<=n ;i++){
        if(cut[i])  nums++;
    }
    cout<<nums<<endl;
    for(int i=1;i<=n ;i++){
        if(cut[i])
        cout<<i<<" ";
    }
    return ;
}
int main (){
//   freopen("in.txt","r",stdin);
//   freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

0x1A BLO (割点)

BLO (割点)
题意: n n n个城镇, m m m条双向道路,没有重复的道路,并且所有城镇连通。
问:把与节点 # i i i 关联的所有边去掉以后(不去掉节点 i i i 本身),无向图有多少个有序点 ( x , y ) (x,y) (x,y),满足 x x x y y y不连通。说明(x,y)和(y,x)是不同的两种
思路:用到了割点, 计算出子树的大小,注意开long long,点和边数比较大。
分两种情况:
1.节点不是割点,结果为 2 ∗ ( n − 1 ) 2*(n-1) 2(n1)
2.节点是割点,结果分为三部分:
one:节点i自身单端构成一个连通块
two: 有t个连通块,分别有搜索树上以 s k ( 1 < = k < = t ) s_k(1<=k<=t) sk(1<=k<=t)为根的子树的节点构成
three:还可能有一个连通块,由除了上述节点之外的所有的点构成。
ACcode详解:

#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define ull unsigned long long
#define rep(i,a,b) for(int i=a;i<=b;i++)
ll gcd(ll a,ll b){ return b? gcd(b,a%b):a;}
const int N=5e5+10;
const ll P=1e9+7;
ll read(){
    ll s = 0, f = 1; char ch = getchar();
    while(!isdigit(ch)){
        if(ch == '-') f = -1;
        ch = getchar();
    }
    while(isdigit(ch)) s = (s << 3) + (s << 1) + (ch ^ 48), ch = getchar();
    return s * f;
}
using namespace std;
struct E{ int to,next;} Edge[N<<1];
int tot=1,head[N];
void AddEdge(int u,int v){
    Edge[++tot] = (E){v,head[u]};
    head[u] = tot;
}
int n,m;
int dfn[N],low[N];
ll ans[N];
int root,num;
ll SIZE[N];   //子树的大小
int cut[N];  
void tarjan(int x){
    dfn[x] = low[x] = ++num;
    SIZE[x] = 1;
    int flag = 0;
    ll sum=1;
    for(int i=head[x];i;i=Edge[i].next){
        int y = Edge[i].to;
        // 是不是搜索树捏
        if(!dfn[y]){
             tarjan(y);
             SIZE[x]+=SIZE[y];
             low[x] = min(low[x],low[y]);
             if(dfn[x] <= low[y]){ // 是可以的
                ans[x] += SIZE[y]*(n-SIZE[y]);  // two
                flag++;
                sum+=SIZE[y];
                if(x!=root || flag>1) cut[x] = 1;  // 是割点   
             }
        }else{
            low[x] = min(low[x],dfn[y]);
        }
    }
    // 注意这里的sum的含义, 不能用SIZE[x]来替代。 sum表示了一个割点子树大小之和。范围的区别.
    if(cut[x]) ans[x]+=(n-sum)*sum;  //three
}
void solve(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;
        cin>>u>>v;
        AddEdge(u,v);
        AddEdge(v,u);
    }
    //所有城镇连通
    root = 1;
    tarjan(1);
    for(int i=1;i<=n;i++){
        if(cut[i]){ // 割点
            ans[i]+= (n-1);   //one
            cout<<ans[i]<<endl;
        }else{
            cout<<2*(n-1)<<endl;
        }
    }
    return ;
}
int main (){
  //freopen("in.txt","r",stdin);
  //freopen("out.txt","w",stdout);
  solve();
  getchar();
  getchar();
  return 0;
}

割边板子 (考虑有重边)

CODE:

struct E {int to,next;} Edge[N<<1];
int tot=1,head[N];
int dfn[N],low[N],bridge[N<<1];
int num;
//割边    
void tarjan(int x,int in_edge){  //  点, 边编号   
    dfn[x] = low[x] = ++num;
    for(int i=head[x];i;i=Edge[i].next){
        int y = Edge[i].to;
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[x],low[y]);
            // 这里和割点不太一样, 是严格大于。 
            //割点是点的所有的边都被删除了,割边只是删除一条边。注意理解割点割边
            if(low[y] > dfn[x]) {  // 割边
                bridge[i] = bridge[i^1] = 1;
            }
        }else if(i!=(in_edge^1)){ // 防止访问同一条边,并不是防父亲节点,重边嘛
            low[x] = min(low[x],dfn[y]);
        }
    }
}
void solve(){
    // 输入......   
    //  全连通 or 不连通
    tarjan(1,0);
    for(int i=2;i<tot;i+=2){ // 加2, tot的初始值为2
        if(bridge[i]){
            printf("%d %d\n",Edge[i^1].to,Edge[i].to);
        }
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

axtices

谢谢您的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值