Week8 作業

A - 區間選點 II

題目

給定一個數軸上的 n 個區間,要求在數軸上選取最少的點使得第 i 個區間 [ai, bi] 裏至少有 ci 個點
使用差分約束系統的解法解決這道題

樣例輸入輸出

Input
輸入第一行一個整數 n 表示區間的個數,接下來的 n 行,每一行兩個用空格隔開的整數 a,b 表示區間的左右端點。1 <= n <= 50000, 0 <= ai <= bi <= 50000 並且 1 <= ci <= bi - ai+1。
Output
輸出一個整數表示最少選取的點的個數
Sample Input
5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1
Sample Output
6

解析

類似的題曾經在前面通過貪心的做法實現過,不過這裏要求要使用差分約束的方法。

所謂差分約束,就是通過一組固定形式的不等式組來約束變量間的關係,其泛型爲:𝑥1 − 𝑥2 ≤ 𝑐 ,c爲常數。這類似於解不等式組操作。

對於泛型的不等式約束,我們通過移項,可以將其變爲 𝑥1 ≤ 𝑐 + 𝑥2, 將c視爲圖中(鏈式前向星描述)的 w(i, j),x1視爲dis [i] , x2視爲dis [j],原式變爲了 𝑑𝑖𝑠[𝑖] ≤ 𝑑𝑖𝑠[𝑗] + 𝑤(𝑖, 𝑗),與最短路的鬆弛操作類似。這樣,我們把xi視作圖中一個節點,對每個約束,視爲從節點 i 到節點 j 連了一條長度爲c的有向邊(注意這裏是有向邊!),這樣問題就變成了上週講過的最短路問題。

當圖中存在負環時(本題),用SPFA算法解決。

對本題,令sum [i] 爲數軸上[ 0, i ] 之間選點的個數,其每個區間 [ ai, bi ] 滿足約束條件如下:
𝑠𝑢𝑚[𝑏i] − 𝑠𝑢𝑚[𝑎i − 1] ≥ 𝑐i

轉化成泛型,跑一遍最長路即可。

代碼

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int n = 50005;
const int m = 1000005;
const int inf = 5e8;

int N = 0, tot = 0, ans = 0;
int head[n], inq[n], dis[n];
queue<int> q;

struct edge{
	int to, next, w;
}edges[m];

void add(int x, int y, int w){
	edges[++tot].to = y;
	edges[tot].next = head[x];
	edges[tot].w = w;
	head[x] = tot;
}


void SPFA(int s){
	for(int i = 1 ; i <= ans ; ++i)
	{
		dis[i] = -inf;
		inq[i] = 0;
	}
	dis[s] = 0;
	inq[s] = 1;
	q.push(s);
	while(!q.empty())
	{
		int u = q.front();
		q.pop();
		inq[u] = 0;
		for(int i = head[u] ; i != 0 ; i = edges[i].next)
		{
			int v = edges[i].to;
			if(dis[v] < dis[u] + edges[i].w)
			{
				dis[v] = dis[u] + edges[i].w;
				if(!inq[v])
				{
					q.push(v);
					inq[v] = 1;
				} 
			}
		}
	}
}

int main(){
	int N;
	cin >> N;
	for(int i = 0 ; i < N ; i++)
	{
		int u, v, w;
		cin >> u >> v >> w;
		add(u, v + 1, w);
		ans = max(ans, v + 1);
	}
	for(int i = 1 ; i <= ans ; i++)
	{
		add(i - 1, i, 0);
		add(i, i - 1, -1);
	}
	SPFA(0);
	cout << dis[ans] << endl;
	return 0;
}

回顧

這題剛開始補充數據超時了,原因是直接套了上次C題的板子,好多沒用的數組初始化沒有刪掉,事實告訴我們不要偷懶…

B - 貓貓向前衝

題目

衆所周知, TT 是一位重度愛貓人士,他有一隻神奇的魔法貓。
有一天,TT 在 B 站上觀看貓貓的比賽。一共有 N 只貓貓,編號依次爲1,2,3,…,N進行比賽。比賽結束後,Up 主會爲所有的貓貓從前到後依次排名併發放愛吃的小魚乾。不幸的是,此時 TT 的電子設備遭到了宇宙射線的降智打擊,一下子都連不上網了,自然也看不到最後的頒獎典禮。
不幸中的萬幸,TT 的魔法貓將每場比賽的結果都記錄了下來,現在他想編程序確定字典序最小的名次序列,請你幫幫他。

樣例輸入輸出

Input
輸入有若干組,每組中的第一行爲二個數N(1<=N<=500),M;其中N表示貓貓的個數,M表示接着有M行的輸入數據。接下來的M行數據中,每行也有兩個整數P1,P2表示即編號爲 P1 的貓貓贏了編號爲 P2 的貓貓。
Output
給出一個符合要求的排名。輸出時貓貓的編號之間有空格,最後一名後面沒有空格!
其他說明:符合條件的排名可能不是唯一的,此時要求輸出時編號小的隊伍在前;輸入數據保證是正確的,即輸入數據確保一定能有一個符合要求的排名。
Sample Input
4 3
1 2
2 3
4 3
Sample Output
1 2 4 3

解析

拓撲排序問題。本題實質上就是給圖中的節點(貓咪)排序。

拓撲排序的方法與求最小生成樹的方法很相似,都是通過隊列來實現。因爲拓撲排序後的起點一定入度爲0(可以通過反證法證明),所以先挑出雖有入度爲零的點,將其入隊,之後依次出隊,遍歷出隊元素可到達的點,然後判斷這些點入度減一,爲0且未到達過則入隊。重複這個過程至隊列空,即可得到結果。

注意拓撲排序的結果不唯一。

代碼

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 50005;
const int M = 250005;

int tot = 0;
int head[N], inq[N], dis[N];
priority_queue<int, vector<int>, greater<int> > q;
vector<int> a;

int in_deg[510];


struct edge{
	int to, next, w;
}edges[M];

void add(int x, int y, int w){
	edges[++tot].to = y;
	edges[tot].next = head[x];
	edges[tot].w = w;
	head[x] = tot;
}

void toposort(int n)
{
	a.clear();
	while(!q.empty())
		q.pop();
	
	for(int i = 1 ; i <= n ; i++)
		if(in_deg[i] == 0)	
			q.push(i);

	while(!q.empty())
	{
		int u = q.top();
		q.pop();
		a.push_back(u);
		for(int i = head[u] ; i != 0 ; i = edges[i].next)
		{
			int v = edges[i].to;
			if(--in_deg[v] == 0)
				q.push(v);
		} 
	}

}

int main()
{
	int n, m;
	while(cin >> n >> m)
	{
		tot = 0; 
		for(int i = 0 ; i <= n ; i++)
		{
			head[i] = 0;
			in_deg[i] = 0;
		}	
		
		for(int i = 0 ; i < m ; i++)
		{
			int p1, p2;
			cin >> p1 >> p2;
			add(p1, p2, 1);
			in_deg[p2]++;
		}
		
		toposort(n);
		
		for(int i = 0 ; i < a.size() - 1 ; i++)
			cout << a[i] << " ";
		cout << a[a.size()-1] << endl;
	}
}

回顧

拓撲排序因爲有之前的算法相印證,所以相對好理解。結合PPT中給出的代碼,很容易就解決了這個問題。

C - 班長競選

題目

大學班級選班長,N 個同學均可以發表意見 若意見爲 A B 則表示 A 認爲 B 合適,意見具有傳遞性,即 A 認爲 B 合適,B 認爲 C 合適,則 A 也認爲 C 合適 勤勞的 TT 收集了M條意見,想要知道最高票數,並給出一份候選人名單,即所有得票最多的同學,你能幫幫他嗎?

樣例輸入輸出

Input
本題有多組數據。第一行 T 表示數據組數。每組數據開始有兩個整數 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下來有 M 行包含兩個整數 A 和 B(A != B) 表示 A 認爲 B 合適。
Output
對於每組數據,第一行輸出 “Case x: ”,x 表示數據的編號,從1開始,緊跟着是最高的票數。 接下來一行輸出得票最多的同學的編號,用空格隔開,不忽略行末空格!
Sample Input
2
4 3
3 2
2 0
2 1
3 3
1 0
2 1
0 2
Sample Output
Case 1: 2
0 1
Case 2: 2
0 1 2

解析

這道題綜合性很強,思考起來也很複雜。構建一個圖來解決它。

首先根據題意我們能夠想到,在這個有向圖中,如果一個人得到了另一個人的支持,那麼它也一定得到了和和那個人處在同一個SCC(強連通分量)中的人的支持。如此,我們把圖分成幾個連通分量。

尋找連通分量,我們通過Kosaraju算法來解決。算法的步驟爲,第一遍dfs1確定原圖的逆後序序列;之後第二遍dfs2,根據逆後序序列依次遍歷每個點,將到達的點標記,每次由起點遍歷到的點即爲一個聯通分量(vis數組標記過的已到達的點將不再計算),所屬的SCC由c數組記錄。算法成立的原理是當圖中的所有邊反向時,強連通分量不受影響。

上面說到,如果一個人得到了另一個人的支持,那麼它也一定得到了和和那個人處在同一個SCC(強連通分量)中的人的支持。那麼,我們把每個強連通分量替換成一個點,將強連通分量間的連通關係視爲點與點之間的連通關係。如此操作後,對於每個點,其支持人數分爲 兩部分,(令 SCC[i] 表示第 i 個 SCC 中點的個數),第一部分爲當前 SCC 中的點,ans += SCC[i] – 1(去除自己),第二部分爲其它 SCC 中的點 SUM ( SCC[j] ),其中 j 可到達 i。

如此,我們便求得了答案。

代碼

#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
vector<int> G1[5005], G2[5005], G3[5005];
int n, c[5005], dfn[5005], vis[5005], dcnt, scnt, indeg[5005];

struct point 
{
	vector<int> v;
}P[5005];

void dfs1(int x)
{
	vis[x] = 1;
	for(int i = 0 ; i < G1[x].size() ; i++)
		if(!vis[G1[x][i]])
			dfs1(G1[x][i]);
	dfn[dcnt++] = x;
}

void dfs2(int x)
{
	c[x] = scnt;
	for(int i = 0 ; i < G2[x].size() ; i++)
		if(!c[G2[x][i]])
			dfs2(G2[x][i]);
}

int dfs3(int x)
{
	vis[x] = 1;
	int y = P[x].v.size();
	for(int i = 0 ; i < G3[x].size() ; i++)
		if(!vis[G3[x][i]])
			y += dfs3(G3[x][i]);
	return y;
}

void kosaraju()
{
	dcnt = scnt = 0;
	memset(c, 0, sizeof c);
	memset(vis, 0, sizeof vis);
	for(int i = 0 ; i < n ; i++)
		if(!vis[i])
			dfs1(i);
	for(int i = n - 1 ; i >= 0; i--)
		if(!c[dfn[i]])
		{
			++scnt;
			dfs2(dfn[i]);
		}
}

void suodian(){
	for(int i =1 ; i <= scnt ; i++)
	{
		G3[i].clear();
		P[i].v.clear();
		indeg[i] = 0;
	}		 
	for(int i = 0 ; i < n ; i++)
	{
		P[c[i]].v.push_back(i);
		for(int j = 0 ; j < G1[i].size() ; j++)
		{
			
			if(c[i] == c[G1[i][j]])
				continue;
			else 
			{
				G3[c[G1[i][j]]].push_back(c[i]);
				indeg[c[i]]++;
			}
		}
	}
}

void SET(){
	for(int i = 1 ; i <= scnt ; i++)
	{
		sort(G3[i].begin(), G3[i].end());
		G3[i].erase(unique(G3[i].begin(), G3[i].end()), G3[i].end());
	}
}

int tag = 1;

int main()
{
	ios::sync_with_stdio(false);
	int T;
	cin >> T;
	while(T--)
	{
		int m;
		cin >> n >> m;
		for(int i = 0 ; i < n ; i++)
		{
			G1[i].clear();
			G2[i].clear();
		}
		while(m--)
		{
			int a, b;
			cin >> a >> b;
			G1[a].push_back(b);
			G2[b].push_back(a);
		}
		//求SCC
		kosaraju();
		//縮點
		suodian();
		//去重 
		SET();
		
		
		int sum[scnt + 1] = {0};
		int ans[5005] = {0};
		
		int Max = 0;
		for(int i = 1 ; i <= scnt ; i++)
		{
			if(!indeg[i])
			{
				for(int j = 1 ; j <= scnt ; j++)
					vis[j] = 0;
				sum[i] = dfs3(i) - 1;
				if(Max < sum[i])	
					Max = sum[i];
			}
		}
		
		int num = 0;
		for(int i = 1 ; i <= scnt ; i++)
		{
			if(sum[i] == Max)
			{
				for(int j = 0 ; j < P[i].v.size() ; j++)
				{
					ans[num] = P[i].v[j];
					num++;
				}
			}
		}
		sort(ans, ans + num); 
		cout << "Case " << tag << ": " << Max << endl;
		tag++;
		for(int i = 1 ; i < num ; i++)
			cout << ans[i - 1] << " ";
		cout << ans[num - 1] << endl;
	}
}

回顧

這道題下課後聽了一遍回放還是能理解的,可是實際操作時代碼部分難度挺大的,瑟瑟發抖。雖然理解了縮點的操作,但是操作起來還是出了很多問題,最後十分艱難才解決。菜雞瑟瑟發抖。

另外這是第一次因爲沒有加 ios::sync_with_stdio(false) 而導致超時的。這個點學長在第一節課說到過,但是沒太在意,沒想到它在某些情況下對時間的影響如此之大。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章