洛谷Oj-資訊傳遞-拓撲排序+DFS/Tarjan強連通分量
阿新 • • 發佈:2019-01-06
問題描述:
有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演算法還很迷,硬撐著寫一點近乎於廢話的註釋。以後再學習吧!