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) 而導致超時的。這個點學長在第一節課說到過,但是沒太在意,沒想到它在某些情況下對時間的影響如此之大。