1174. 受欢迎的牛

每一头牛的愿望就是变成一头最受欢迎的牛。

现在有 N 头牛,编号从 1 到 N,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。

这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。

你的任务是求出有多少头牛被除自己之外的所有牛认为是受欢迎的。

输入格式

第一行两个数 N,M;

接下来 M 行,每行两个数 A,B,意思是 A 认为 B 是受欢迎的(给出的信息有可能重复,即有可能出现多个 A,B)。

输出格式

输出被除自己之外的所有牛认为是受欢迎的牛的数量。

数据范围

1≤N≤104,
1≤M≤5×104

输入样例:
3 3
1 2
2 1
2 3
输出样例:
1
样例解释

只有第三头牛被除自己之外的所有牛认为是受欢迎的。

思路:
/*
有向图
连通分量:对于分量中任意两点u,v 必然可以从u走到v 且从v走到u
强连通分量:极大连通分量
有向图   →   有向无环图(DAG)
        缩点(将所有连通分量缩成一个点)
缩点举例:
   o→o→o→o
     ↑ ↓
   o→o→o→o
中间的环缩成一个点
o     o
  ↘ ↗
   o
  ↗ ↘ 
o     o
应用:
求最短/最长路 递推

求强连通分量:dfs
1 树枝边(x,y)
        o
       / \
      o   o
     /   / \
    o   o   o
2 前向边(x,y)
        o
       / \
      o   x
     /  ↙ \
    o   y   o
3 后向边
        o
       / \
      o   y
     /  ↗ \
    o   x   o
4横插边(往已经搜过的路径上的点继续深搜)
因为我们是从左往右搜的 所以一般是x左边分支上的点
        o
       / \
      o   o
     /   / \
    y ← x   o
如果往x右边边分支上的点搜 则属于树枝边

强连通分量:
情况1:
    x存在后向边指向祖先结点y 直接构成环
        o
       / \
      o   y
     /  ↗/\
    o   x   o
情况2:
    x存在横插边指向的点y有指向x和y的公共祖先节点及以上的点的边
    再通过根节点往下走到x间接构成环
        o
   ↗  / \
  ↗  o   o
 ↗  /   / \
    y ← x   o

Tarjan 算法求强连通分量
引入 时间戳(按dfs 回溯的顺序标记)
        1
   ↗  / \
  ↗  2   4  
 ↗  /   / \
    3 ← 5   6
标记上时间后:
dfn[u]dfs遍历到u的时间(如上图中的数字)
low[u]从u开始走所能遍历到的最小时间戳(上图中1,2,3,4,5都是一个环/强连通分量中的
                                    即dfn[1]=low[1]=low[2]=low[3]=low[4]=low[5])
    --即u如果在强连通分量,其所指向的层数最高的点

u是其所在的强连通分量的最高点 (上图中dfn[1]=low[1] dfn[6]=low[6])
           <=> 
     dfn[u] == low[u]

树枝边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
前向边(x,y) 中dfn[y]>dfn[x] low[u]>dfn[u]
后向边(x,y) 中dfn[x]>dfn[y] 后向边的终点dfn[u] == low[u]
横插边(x,y) 中dfn[x]>dfn[y] 

缩点
for i=1;i<=n;i++
 for i的所有邻点j
   if i和j不在同一scc中:
    加一条新边id[i]→id[j]

缩点操作后变成有向无环图
就能做topo排序了(此时连通分量编号id[]递减的顺序就是topo序了)
因为我们++scc_cnt是在dfs完节点i的子节点j后才判断low[u]==dfn[u]后才加的
        那么子节点j如果是强连通分量 scc_idx[j]一定小于scc_idx[i]

线性复杂度 一遍dfs就行
*/

代码:
/*
本题 
当一个强连通的出度为0,则该强连通分量中的所有点都被其他强连通分量的牛欢迎
但假如存在两及以上个出度=0的牛(强连通分量) 则必然有一头牛(强连通分量)不被所有牛欢迎
见下图最右边两个强连通分量
o→o→o
  ↑
  o→o
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, M = 50010;

int n, m;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
/*
在一个有向的拓扑图,两点之间是不一定联通的,也就是说从一个点出发走不到所有的点。所以做一次tarjan算法从起点出发缩点的范围是起点能够走到的所有点,这样操作之后每一个从起点出发的缩点就是非并行的。
比如后遍历的起点dfn是50,但是搜到一个之前已经遍历过的dfn为1的点,这时候显然是一个前向边,满足拓扑序的,但是low[u]就会全部更新成为u,就很僵硬。
*/
int id[N], scc_cnt, Size[N];
int dout[N];
void add(int a, int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}

void tarjan(int u) // 缩点的过程
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u, in_stk[u] = true;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j]) // 没访问过就继续
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j]) // 访问过看看标记是不是比当前值小
            low[u] = min(low[u], dfn[j]);
    }

    if (dfn[u] == low[u]) //该强连通分量第一个访问的点
    {
        int y;
        ++scc_cnt;
        do
        {
            y = stk[top--];
            Size[scc_cnt]++;
            id[y] = scc_cnt;
            in_stk[y] = false;
        } while (y != u);
    }
}

int main()
{
    memset(h, -1, sizeof(h));
    scanf("%d %d", &n, &m);
    while (m--)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b);
    }

    for (int i = 1; i <= n; i++) // 深度优先
    {
        if (!dfn[i])
        {
            tarjan(i);
        }
    }

    for (int u = 1; u <= n; u++) // 统计出度为0的强连通分量 后面累加
    {
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (id[u] != id[j])
            {
                dout[id[u]]++;
            }
        }
    }

    int zeros = 0, res = 0;
    for (int i = 1; i <= scc_cnt; i++)
    {
        if (!dout[i]) // 只需要判断是否等于0 所以强连通分量之间有多条边也无所谓
        {
            zeros++;
            res += Size[i];
            if (zeros > 1)
            {
                res = 0;
                break;
            }
        }
    }

    printf("%d\n", res);

    return 0;
}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

追寻远方的人

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值