洛谷Oj-信息传递-拓扑排序+DFS/Tarjan强连通分量

来源:互联网 发布:网络视频服务器的安装 编辑:程序博客网 时间:2024/05/17 22:11

问题描述:
有n个同学(编号为1到n)正在玩一个信息传递的游戏。在游戏里每人都有一个固定的信息传递对象,其中,编号为i的同学的信息传递对象是编号为Ti同学。
游戏开始时,每人都只知道自己的生日。之后每一轮中,所有人会同时将自己当前所知的生日信息告诉各自的信息传递对象(注意:可能有人可以从若干人那里获取信息,但是每人只会把信息告诉一个人,即自己的信息传递对象)。当有人从别人口中得知自己的生日时,游戏结束。请问该游戏一共可以进行几轮?
80分代码:

struct edge//链式前向星{    int to;//终点    int next;//下一条边};edge e[200010];//边集数组int n,head[200010];int cnt = 1;int ans = inf;//求最小环,所以将答案初始化为无穷大bool loop[200010];//标记void add_edge(int x,int y)//加边{    e[cnt].to = y;    e[cnt].next = head[x];    head[x] = cnt;    cnt++;}int bfs(int x)//从起点x出发找环{    int sum = 1;//将顶点x计入    int book[200010];//标记,防止进入一个环后陷入死循环    for(int i = 1; i <= n; ++i)//一个小优化,可能会比memset快        book[i] = 0;    queue<int> q;//队列    q.push(x);//入队    book[x] = 1;//标记    while(!q.empty())    {        int t = q.front();//访问        for(int i = head[t]; i != -1; i = e[i].next)//遍历每一条以t为起点的边        {            if(e[i].to == x)//如果回到了x                return sum;            if(book[e[i].to] == 0)//如果没被标记过            {                q.push(e[i].to);//入队                book[e[i].to] = 1;//标记                sum++;//累加                if(sum > ans)//最优化剪枝                    return inf;            }        }        q.pop();//出队    }    return inf;//返回一个不影响答案的值}void mark(int x)//标记{    queue<int> q;//队列    q.push(x);//入队    loop[x] = true;//标记    while(!q.empty())    {        int t = q.front();//访问        for(int i = head[t]; i != -1; i = e[i].next)//遍历每一条以t为起点的边        {            if(e[i].to == x)//回到x                return;            q.push(e[i].to);//入队            loop[e[i].to] = true;//标记        }        q.pop();//出队    }}int main(){    cin >> n;//输入    memset(head,-1,sizeof(head));//初始化    for(int i = 1; i <= n; ++i)    {        int to;        scanf("%d",&to);        add_edge(i,to);//加边    }    for(int i = 1; i <= n; ++i)    {        if(loop[i] == true)//如果该点在一个环内            continue;        int t = bfs(i);//记录        if(t != inf)//环存在            mark(i);//标记环上的每一个点        ans = min(ans,t);//更新答案    }    cout << ans << endl;    return 0;}

代码②:拓扑排序+DFS

struct edge{    int to;    int next;};edge e[200010];int n,head[200010],in_degree[200010],book[200010],t;//数组in_degree记录每个顶点的入度,数组book用来标记该点是否处于环中int cnt = 1;void add_edge(int x,int y)//加边{    e[cnt].to = y;    e[cnt].next = head[x];    head[x] = cnt;    cnt++;}void topology_sort()//拓扑排序{    queue<int> q;//队列    for(int i = 1; i <= n; ++i)//找出入度为0的顶点,将其入队        if(in_degree[i] == 0)        {            q.push(i);//入队            book[i] = 1;//标记(删去该点)        }    while(!q.empty())    {        int t = q.front();//访问队首        for(int i = head[t]; i != -1; i = e[i].next)//以顶点t为起点的所有边        {            in_degree[e[i].to]--;//其终点入度减1(因为顶点t已经被删去了)            if(in_degree[e[i].to] == 0)//如果入度为0            {                q.push(e[i].to);//入队                book[e[i].to] = 1;//标记(删去该点)            }        }        q.pop();//出队,别忘了,否则会死循环    }}void dfs(int x){    for(int i = head[x]; i != -1; i = e[i].next)//遍历以顶点x为起点的所有边        if(book[e[i].to] == 0)//如果其终点没被删去(说明其终点处于环中)        {            t++;//环的大小加1            book[e[i].to] = 1;//标记已经被搜过            dfs(e[i].to);//继续深搜        }}int main(){    cin >> n;//输入    memset(head,-1,sizeof(head));//初始化    for(int i = 1; i <= n; ++i)    {        int to;        scanf("%d",&to);        add_edge(i,to);//建边        in_degree[to]++;//终点to的入度加1    }    topology_sort();//拓扑排序    int ans = inf;//初始化    for(int i = 1; i <= n; ++i)        if(book[i] == 0)//如果不是链        {            t = 0;//重置            dfs(i);//深搜,t的值改变            ans = min(ans,t);        }    cout << ans << endl;    return 0;}

代码③:Tarjan求强连通分量

struct edge{    int to;    int next;};edge e[200010];int n,head[200010];int dfn[200010],low[200010],book[200010],ts;//数组dfn记录每个顶点的时间戳,数组low记录每个顶点能访问到的顶点中的时间戳的最小值,数组book用来标记,ts为timestamp(时间戳)stack<int> s;//栈,存放强联通分量中的顶点int cnt = 1;int ans = inf;void add_edge(int x,int y)//加边{    e[cnt].to = y;    e[cnt].next = head[x];    head[x] = cnt;    cnt++;}void Tarjan(int x){    book[x] = 1;//标记    ts++;//时间戳自增    dfn[x] = ts;    low[x] = ts;    s.push(x);//入栈    for(int i = head[x]; i != -1; i = e[i].next)//访问以顶点x为起点的所有边    {        int t = e[i].to;//终点        if(dfn[t] == 0)//如果没被搜索过        {            Tarjan(t);//搜索            low[x] = min(low[x],low[t]);        }        if(book[t] == 1)            low[x] = min(low[x],dfn[t]);    }    if(low[x] == dfn[x])    {        int res = 0;        while(s.top() != x)        {            book[s.top()] = 0;            s.pop();//出栈            res++;//大小+1        }        book[s.top()] = 0;        s.pop();//出栈        res++;//大小+1        if(res != 1)//不能是自环            ans = min(ans,res);    }}int main(){    cin >> n;//输入    memset(head,-1,sizeof(head));//初始化    for(int i = 1; i <= n; ++i)    {        int to;        scanf("%d",&to);        add_edge(i,to);//加边    }    for(int i = 1; i <= n; ++i)        if(dfn[i] == 0)//如果时间戳为0,即之前没有搜索过该点            Tarjan(i);//搜索    cout << ans << endl;    return 0;}

解决方法:
环可能不止一个,所以要求出最小的一个环
要注意由一个点进入环后,如果不标记的话,广搜会死循环
每进行一轮,信息就沿着环流动一次,环有多少条边(顶点),游戏就会进行几轮。
如果一个点在环内,那么下一次就没必要对环内的点进行搜索。比如2->3->4->2,就没必要对3,4进行搜索
分析发现,建图后的情况只能由两种:环,链+环。不存在一条链的情况,因为链的终点是没有出度的。
我们只想求环的大小,如果链过长,就会产生大量时间消耗,每次都可能搜索一条长链后才搜索到环
我们可以联系到拓扑排序的思想,从一个入度为0的点开始,删点,删边,再删点…直到将链删去
之后每次搜索我们就是只对环进行搜索了
Tarjan算法还很迷,硬撑着写一点近乎于废话的注释。以后再学习吧!

原创粉丝点击