1. 程式人生 > >洛谷Oj-資訊傳遞-拓撲排序+DFS/Tarjan強連通分量

洛谷Oj-資訊傳遞-拓撲排序+DFS/Tarjan強連通分量

問題描述:
有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演算法還很迷,硬撐著寫一點近乎於廢話的註釋。以後再學習吧!