[jzoj]7.13自主學習報告
目錄
不知道是什麼原因,今天突然沒比賽了,好不習慣。還好,老師講了一下Tarjan,spfa以及並查集。
於是乎,我就翻遍了jzoj做了幾道題,來給大家分享分享。
1.Tarjan
1.親戚
題目
若某個家族人員過於龐大,要判斷兩個是否是親戚,確實還很不容易,現在給出某個親戚關係圖,求任意給出的兩個人是否具有親戚關係。
規定:x和y是親戚,y和z是親戚,那麼x和z也是親戚。如果x,y是親戚,那麼x的親戚都是y的親戚,y的親戚也都是x的親戚。
輸入:
第一行:三個整數n,m,p,(n<=5000,m<=5000,p<=5000),分別表示有n個人,m個親戚關係,詢問p對親戚關係。
以下m行:每行兩個數Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有親戚關係。
接下來p行:每行兩個數Pi,Pj,詢問Pi和Pj是否具有親戚關係。
輸出:
P行,每行一個’Yes’或’No’。表示第i個詢問的答案為“具有”或“不具有”親戚關係。
思路:
這道題不就是並查集的模板題嗎?愛動腦的我寫完並查集後,突然腦子一轉,發現這道題也可以用tarjan縮點啊!
簡單來說,tarjan就是把每一塊強聯通分量縮成一個點,使整個有向圖變成一個dag圖。
對於這一道題,可以採用tarjan縮點,使整個親戚關係圖中每一個強聯通分量縮成一個點(為了方便解釋,以下把強聯通分量縮成的點簡稱為“塊”),這樣一來,屬於同一“塊”的兩個點一定互為親戚(不難理解),不屬於同一“塊”的兩個點就不是親戚了。
不同於普通縮點題的是,這道題的親戚關係圖並不是有向圖,而是無向圖,所以對於每一條邊(u,v),要連上u->v和v->u,實現上並不難,我採用鏈式前向星存圖。
Code:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=5005; struct edge { int v,next; }e[maxn*2]; //鏈式前向星,由於是無向圖,邊的數量是m*2 int head[maxn]; int dfn[maxn],low[maxn]; //tarjan的陣列 int stack[maxn],ind; //用棧儲存點 bool visit[maxn];//visit[i]表示第i個點是否在棧中 int color[maxn]; //縮點後每一個點屬於的“塊” int n,m,p,cnt,tot,num; void Init() { //初始化 memset(head,-1,sizeof(head)); for(int i=1;i<=m;i++) e[i].v=e[i].next=0; cnt=0; } void Add(int u,int v) { //加邊 e[++cnt].v=v; e[cnt].next=head[u]; head[u]=cnt; } void Tarjan(int x) { //tarjan縮點模板 dfn[x]=low[x]=++tot; stack[++ind]=x; visit[x]=true; for(int i=head[x];i!=-1;i=e[i].next) { int v=e[i].v; if(!dfn[v]) { Tarjan(v); low[x]=min(low[x],low[v]); } else if(visit[v]) { low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]) { //如果這個點是根節點 num++; while(stack[ind+1]!=x) { int k=stack[ind--]; color[k]=num; } //將屬於這個強聯通分量的點染成同一種顏色 } } int main() { Init(); scanf("%d %d %d",&n,&m,&p); for(int i=1;i<=m;i++) { int a,b; scanf("%d %d",&a,&b); Add(a,b); //由於是無向圖,所以要加兩條邊 Add(b,a); } for(int i=1;i<=n;i++) //對每一個點進行搜尋 if(!dfn[i]) Tarjan(i); for(int i=1;i<=p;i++) { int a,b; scanf("%d %d",&a,&b); if(color[a]==color[b]) //屬於同一個“塊” printf("Yes\n"); else printf("No\n"); } return 0; }
2.Network of Schools
題目
A number of schools are connected to a computer network. Agreements have been developed among those schools: each school maintains a list of schools to which it distributes software (the “receiving schools”). Note that if B is in the distribution list of school A, then A does not necessarily appear in the list of school B
You are to write a program that computes the minimal number of schools that must receive a copy of the new software in order for the software to reach all schools in the network according to the agreement (Subtask A). As a further task, we want to ensure that by sending the copy of new software to an arbitrary school, this software will reach all schools in the network. To achieve this goal we may have to extend the lists of receivers by new members. Compute the minimal number of extensions that have to be made so that whatever school we send the new software to, it will reach all other schools (Subtask B). One extension means introducing one new member into the list of receivers of one school.
輸入:
The first line contains an integer N: the number of schools in the network (2 <= N <= 100). The schools are identified by the first N positive integers. Each of the next N lines describes a list of receivers. The line i+1 contains the identifiers of the receivers of school i. Each list ends with a 0. An empty list contains a 0 alone in the line.
輸出:
Your program should write two lines to the standard output. The first line should contain one positive integer: the solution of subtask A. The second line should contain the solution of subtask B.
思路&題意:
題意:
一個包含1-n號學校的網路,每個學校有個軟體分發列表,當學校拿到軟體時會把軟體分發給列表裡的學校。
問1:一個新軟體出現時初始化情況至少需要給多少個學校才能讓它到達整個網路?
問2:至少需要新增多少個名單才能使從任意一個學校開始分發都能充滿整個網路?
轉成圖的理解就是↓
—給定一個有向圖,求:
1) 至少要選幾個頂點,才能做到從這些頂點出發,可以到達全部頂點
2) 至少要加多少條邊,才能使得從任何一個頂點出發,都能到達全部頂點— 頂點數<= 100
思路:
1. 求出所有強連通分量
2. 每個強連通分量縮成一點,則形成一個有向無環圖DAG。
3. DAG上面有多少個入度為0的頂點,問題1的答案就是多少
在DAG上要加幾條邊,才能使得DAG變成強連通的,問題2的答案就是多少
加邊的方法:
要為每個入度為0的點新增入邊,為每個出度為0的點添加出邊
假定有 n 個入度為0的點,m個出度為0的點,如何加邊?
把所有入度為0的點編號 0,1,2,3,4 ….N -1
每次為一個編號為i的入度0點可達的出度0點,新增一條出邊,連到編號為(i+1)%N 的那個出度0點, 這需要加n條邊 。
若 m <= n,則加了這n條邊後,已經沒有入度0點,則問題解決,一共加了n條邊
若 m > n,則還有m-n個入度0點,則從這些點以外任取一點,和這些點都連上邊,即可,這還需加m-n條邊。
所以,max(m,n)就是第二個問題的解
此外:當只有一個強連通分支的時候,就是縮點後只有一個點,雖然入度出度為0的都有一個,但是實際上不需要增加清單的項了,所以答案是1,0;
Code:
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
#define SpeedUp std::cout.sync_with_stdio(false);
const int maxn = 105;
const int maxm = maxn * (maxn - 1) / 2;
int head[maxn];
int eCnt;
struct Edge
{
int v, next;
} edge[maxm];
void add(int u, int v) {
edge[eCnt].v = v, edge[eCnt].next = head[u], head[u] = eCnt++;
}
int dfn[maxn], low[maxn];
int vis[maxn], inStack[maxn], _stack[maxn];
int rt[maxn]; //所在的強連通分量的序號
int rtCnt; //強連通分量數目
int idx; //遍歷順序編號
int top; //棧索引
void Tarjan(int u) {
dfn[u] = low[u] = idx++;
vis[u] = inStack[u] = 1;
_stack[top++] = u;
for (int i = head[u]; ~i; i = edge[i].next) {
int v = edge[i].v;
if (!vis[v]) {
Tarjan(v);
low[u] = min(low[u], low[v]);
} else if (inStack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) { //連通分量根
rtCnt++;
while (top > 0 && _stack[top] != u) {
top--;
int v = _stack[top];
rt[v] = rtCnt; //賦值為u的話就成並查集了
inStack[v] = 0;
}
}
}
int ind[maxn], outd[maxn]; //強連通分量的出入度
int main() {
int n, to;
// RE
while (cin >> n) {
top = idx = 1;
rtCnt = 0;
clr(_stack, 0);
clr(inStack, 0);
clr(vis, 0);
clr(head, -1);
// clr(dfn,0);clr(low,0);clr(rt,0); //沒必要
for (int i = 1; i <= n; ++i) {
cin >> to;
while (to) {
add(i, to);
cin >> to;
}
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
Tarjan(i);
}
}
//遍歷每個點的每條邊,如果兩頂點不是一個分量裡的則統計出入度
//若結構體edge也存起點的話可以直接遍歷邊數
clr(ind, 0);
clr(outd, 0);
for (int u = 1; u <= n; ++u) {
for (int i = head[u]; ~i; i = edge[i].next) {
if (rt[edge[i].v] != rt[u]) {
ind[rt[edge[i].v]]++;
outd[rt[u]]++;
}
}
}
//ans1 = 入度為0的強聯通分量,ans2 = max(入度0分量數,出度0分量數)
int cntIn = 0, cntOut = 0;
for (int i = 1; i <= rtCnt; ++i) {
if (!ind[i])
cntIn++;
if (!outd[i])
cntOut++;
}
cout << cntIn << endl;
if (rtCnt == 1) //一個分量時不需要補充邊
cout << "0" << endl;
else
cout << max(cntIn, cntOut) << endl;
}
return 0;
}
2.spfa
Travel
題目
小Q非常喜歡在自己的國家旅行。小Q所在的國家有N座城市,分別編號為1~n,小Q所在的城市編號為1。小Q現在想知道從他所在的城市出發,到其他N-1個城市的最短路程分別是多少?
輸入:
第一行兩個整數N,M(1<=n<=1000,1<=M<=100000),分別表示小Q所在的國家有N座城市以及城市間有M條單向道路。
接下來M行,每行三個整數x,y,len(1<=x,y<=n,1<=len<=100000)表示從城市x去到城市y需要走len這麼多路程。
輸入可能存在重邊
輸出:
一共N-1行,每行一個整數,第i個整數表示小Q從城市1到城市(i+1)的最短路程。如果不能到達輸出-1。
思路:
明顯的最短路,SPFA秒掉。注意是單向。
Code:
#include <algorithm>
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cmath>
#define maxint 2139062143
using namespace std;
int a[1010][1010],dist[100010],n;
void spfa(int s)
{
int q[100010],v[100010],h = 0,t = 0,x,i;
memset(q,0,sizeof(q));
memset(v,0,sizeof(v));
memset(dist,127,sizeof(dist));
dist[s] = 0;
q[t++] = s;
v[s] = 1;
while(h != t)
{
x = q[h++];
v[x] = 0;
for(i = 1;i <= n;i++)
{
if(dist[i] - a[x][i] > dist[x])
{
dist[i] = dist[x] + a[x][i];
if(!v[i])
{
q[t++] = i;
v[i] = 1;
}
}
}
}
}
int main()
{
int m;
scanf("%d%d",&n,&m);
memset(a,127,sizeof(a));
for(register int i = 1;i <= m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
a[x][y] = min(a[x][y],z);
}
spfa(1);
for(register int i = 2;i <= n;i++)
printf("%d\n",dist[i] == 0x3f3f3f3f ? -1 : dist[i]);
return 0;
}
int q[100010],v[100010],h = 0,t = 0,x,i;
memset(q,0,sizeof(q));
memset(v,0,sizeof(v));
memset(dist,127,sizeof(dist));
dist[s] = 0;
q[t++] = s;
v[s] = 1;
while(h != t)
{
x = q[h++];
v[x] = 0;
for(i = 1;i <= n;i++)
{
if(dist[i] - a[x][i] > dist[x])
{
dist[i] = dist[x] + a[x][i];
if(!v[i])
{
q[t++] = i;
v[i] = 1;
}
}
}
}
}
int main()
{
int m;
scanf("%d%d",&n,&m);
memset(a,127,sizeof(a));
for(register int i = 1;i <= m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
a[x][y] = min(a[x][y],z);
}
spfa(1);
for(register int i = 2;i <= n;i++)
printf("%d\n",dist[i] == 0x3f3f3f3f ? -1 : dist[i]);
return 0;
}
3.並查集
1.親戚
題目
若某個家族人員過於龐大,要判斷兩個是否是親戚,確實還很不容易,現在給出某個親戚關係圖,求任意給出的兩個人是否具有親戚關係。
規定:x和y是親戚,y和z是親戚,那麼x和z也是親戚。如果x,y是親戚,那麼x的親戚都是y的親戚,y的親戚也都是x的親戚。
輸入:
第一行:三個整數n,m,p,(n<=5000,m<=5000,p<=5000),分別表示有n個人,m個親戚關係,詢問p對親戚關係。
以下m行:每行兩個數Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有親戚關係。
接下來p行:每行兩個數Pi,Pj,詢問Pi和Pj是否具有親戚關係。
輸出:
P行,每行一個’Yes’或’No’。表示第i個詢問的答案為“具有”或“不具有”親戚關係。
思路:
這道題目就是一個並查集稍加修飾的一個板子題。從題目描述中我們可以知道,若兩個人是親戚關係,那麼他們一定在同一個集合裡面。那麼就ok了,對於兩個親戚關係的人我們進行 並 的操作,然後對於兩個判斷的人就找他們的根結點就ok了。是不是就,很簡單了?頃刻之間就,做出來了。
ps:是不是覺得這道題很眼熟?想必你們也知道了:我做了兩種解法!
Code:
#include<bits/stdc++.h>
using namespace std;
int n,m,p,father[5200];
//n,m,p的解釋見題,father陣列用來記錄該結點屬於哪個集合(儲存根結點/父親結點)
struct node
{
int x,y;
}a[5200];
//a陣列用來儲存每對親戚關係。
int getfather(int k)
{
if(father[k]==k)return k;
//如果一個點的父親是自己,那麼它就是根結點
father[k]=getfather(father[k]);
//這裡是記憶化,壓縮路徑。
return father[k];
}
//getfather就是找到該結點的根結點,並記憶
void bing(int k,int l)
{
int fx,fy;
fx=getfather(k);
fy=getfather(l);
father[fx]=fy;
}
//並操作。對兩個點的根結點進行如下操作:一個根結點變成另外一個點的兒子結點。那麼兩個集合就併為一個集合了。
bool cha(int k,int l)
{
int fx,fy;
fx=getfather(k);
fy=getfather(l);
return fx==fy;
}
//查操作。求出兩個結點的根結點,如果相同,那麼他們屬於一個集合。
void read()
{
scanf("%d%d%d",&n,&m,&p);
int x,y;
for(int i=1;i<=n;i++)
father[i]=i;
//每個結點的根結點預設是自己(這就可以理解為有n個集合,每個點是一個集合)
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
bing(x,y);
//讀入親戚關係後,並他。
}
}
//讀入
void work()
{
int x,y;
for(int i=1;i<=p;i++)
{
scanf("%d%d",&x,&y);
if(cha(x,y))printf("Yes\n");
else printf("No\n");
//讀入後查詢
}
}
int main()
{
read();//讀入
work();//工作
return 0;
}
2.食物鏈
題目
動物王國中有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B, B吃C,C吃A。
現有N個動物,以1-N編號。每個動物都是A,B,C中的一種,但是我們並不知道它到底是哪一種。
有人用兩種說法對這N個動物所構成的食物鏈關係進行描述:
第一種說法是“1 X Y”,表示X和Y是同類。
第二種說法是“2 X Y”,表示X吃Y。
此人對N個動物,用上述兩種說法,一句接一句地說出K句話,這K句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。
1) 當前的話與前面的某些真的話衝突,就是假話;
2) 當前的話中X或Y比N大,就是假話;
3) 當前的話表示X吃X,就是假話。
你的任務是根據給定的N(1<=N<=50,000)和K句話(0<=K<=100,000),輸出假話的總數。
輸入:
第一行是兩個整數N和K,以一個空格分隔。
以下K行每行是三個正整數 D,X,Y,兩數之間用一個空格隔開,其中D表示說法的種類。
若D=1,則表示X和Y是同類。
若D=2,則表示X吃Y。
輸出:
只有一個整數,表示假話的數目。
思路:
1.p[x]表示x根結點。r[x]表示p[x]與x關係。r[x]=0 表示p[x]與x同類;1表示p[x]吃x;2表示x吃p[x]。
2.怎樣劃分一個集合呢?
注意,這裡不是根據x與p[x]是否是同類來劃分。而是根據“x與p[x]能否確定兩者之間關係”來劃分,若能確定x與p[x]關係,則它們同屬一個集合
3.怎樣判斷一句話是不是假話?
假設已讀入D ,X ,Y ,先利用find()函式得到X,Y所在集合代表元素fx,fy,若它們在同一集合(即fx==fy)則可以判斷這句話真偽:
若 D==1 而 r[X]!=r[Y] 則此話為假.(D==1 表示X與Y為同類,而從r[X]!=r[Y]可以推出 X 與 Y 不同類.矛盾.)
若 D==2 而 r[X]==r[Y](X與Y為同類)或者r[X]==(r[Y]+1)%3(Y吃X)則此話為假。
4.上個問題中r[X]==(r[Y]+1)%3這個式子怎樣推來?
假設有Y吃X,那麼r[X]和r[Y]值是怎樣?
我們來列舉一下:
r[X]=0&&r[Y]=2
r[X]=1&&r[Y]=0
r[X]=2&&r[Y]=1
稍微觀察一下就知道r[X]=(r[Y]+1)%3;
事實上,對於上個問題有更一般判斷方法:
若(r[Y]-r[X]+3)%3!=D-1 ,則此話為假.
5.其他注意事項:
在unionn(d,x,y)過程中若將S(fy)合併到S(fx)上,則相應r[fy]必須更新為fy相對於fx關係。怎樣得到更新關係式?
r[fy]=(r[x]-r[y]+d+3)%3;
程式碼實現如下
Code:
#include <cstdio>
using namespace std;
int p[50001],r[50001],n,k,ans,x,y,d;
int find(int x)
{
if (x != p[x])
{
int fx = find(p[x]);
r[x] = (r[x] + r[p[x]]) % 3;
p[x] = fx;
}
return p[x];
}
bool unionn(int d,int x,int y)
{
int fx = find(x),fy = find(y);
if (fx == fy)
if ((r[y] - r[x] + 3) % 3 != d)
return 1;
else
return 0;
p[fy] = fx;
r[fy] = (r[x] - r[y] + d + 3) % 3;
return 0;
}
int main()
{
scanf("%d %d",&n,&k);
for (int i = 1;i <= n;i++)
p[i] = i,r[i] = 0;
for (int i = 1;i <= k;i++)
{
scanf("%d %d %d",&d,&x,&y);
if(x > n || y > n || (x == y && d == 2))
{
ans++;
continue;
}
if (unionn(d - 1,x,y))
ans++;
}
printf("%d",ans);
}