1. 程式人生 > 實用技巧 >暑期集訓模擬賽3

暑期集訓模擬賽3

前言

今天除了改成\(0\)分的\(T4\)一切安好……

NO.1 中中救援隊

原型:安慰奶牛

題目描述

中中酷愛滑雪,某日突發奇想,帶領所有\(BDEZ\)\(OIER\)\(Alps\)滑雪,不幸的是,中中和\(OIER\)們遭遇了雪崩,除了中中,所有的\(OIER\)們都埋在了雪坑裡,此時,中中救援隊閃亮登場~!(中中救援隊只有中中一個人!\(Orz\)!)

雪崩之後,出現了\(N\)個雪坑,每個雪坑都有一名\(OIER\)深陷其中,只有中中倖免,現在中中找到了M條雙向道路,每條道路都會連線兩個雪坑,但是,由於中中是路痴(-_-||),所以中中希望去除\(M\)條道路中儘可能多的道路,但是還要保證任一雪坑都能到達其他的雪坑,所以要先選擇留下\(N-1\)

條道路,中中可以從任意一個雪坑出發,並且任務完成後要回到出發點(起點的雪坑和終點的雪坑也需要消耗體力),而且中中只記得他選擇的道路 中中每到一個雪坑\(i\),都會消耗一定的體力值\(t_i\),即使這個雪坑的\(OIER\)已被救出。第\(j\)條道路連線\(x,y\)兩個雪坑,而從\(x\)到達\(y\)也需要消耗體力值\(energy\) 由於時間緊迫,中中請你這名\(OIER\)幫助他計算下救出這\(N\)\(OIER\)所消耗的最小體力值是多少

輸入格式

輸入第一行兩個整數\(N\)\(M\),表示有\(N\)\(OIER\)\(M\)條連線的道路

接下來\(N\)行,每行一個整數\(t_i\)

,表示第\(i\)個雪坑需要消耗中中的體力值

然後\(M\)行,每行三個整數\(x,y,energy\),表示從\(x\)坑滑到\(y\)坑需要消耗的體力值為\(energy\)

輸出格式

\(1\)行,一個整數,為中中消耗的最小體力值

樣例

樣例輸入

5 7
6
5
13
8
18
4 1 7
5 2 5
1 5 16
2 3 20
3 1 18
4 3 12
2 4 15

樣例輸出

154

資料範圍與提示

對於\(30\%\)的資料 \(5\le N\le 100,N-1\le M\le 500 1\le t_i\le 100 x\le N,y\le N,1\le energy\le 100\)

對於全部資料 \(5\le N\le 10000,N-1\le M\le 100000 1\le t_i\le 1000 x\le N,y\le N,1\le energy\le 1000\)

結果保證不超過\(2^{31}-1\)

分析

看到題目中只保留\(N-1\)條邊,且要求消耗能量最小即路徑長最小,那麼就是一個最小生成樹板子題了,但是處理邊權需要考慮一下。
因為每條路徑一定要經過兩遍,所以首先需要讓路徑長乘以\(2\),又因為每個點的點權也需要計算,所以我們還需要加上路徑兩端的點權,然後跑最小生成樹就行了。
因為起點和終點也要計算,而除了終點都是經過兩次,又因為處理的時候沒有把每個起點和重點的值計算兩邊,所以需要找出最小的點權,並且從那裡作為起點,最後就是最小生成樹的值加上這個最小的點權。

程式碼



#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e4+10;
struct Node{
	int x,y,val;
}e[maxn*10];
int n,m;
int mn=0x7f7f7f7f;
int t[maxn],fa[maxn];
bool cmp(Node a,Node b){
	return a.val<b.val;
}
int Find(int x){//並查集
	return x==fa[x]?x:(fa[x]=Find(fa[x]));
}
int kruscal(){//最小生成樹板子……
	int ans = 0;
	sort(e,e+m,cmp);
	for(int i=0;i<m;++i){
		int x = Find(e[i].x);
		int y = Find(e[i].y);
		if(x != y){
			fa[x] = y;
			ans += e[i].val;
		}
	}
	return ans;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=0;i<n;++i)fa[i]=i;
	for(int i=0;i<n;++i){
		scanf("%d",&t[i]);
	}
	for(int i=0;i<m;++i){
		scanf("%d%d%d",&e[i].x,&e[i].y,&e[i].val);
		e[i].val = 2*e[i].val+t[e[i].x-1]+t[e[i].y-1];//處理邊權
	}
	for(int i=0;i<n;++i){//找到最小點權
		if(mn>t[i])mn=t[i];
	}
	int ans = mn+kruscal();//計算答案
	printf("%d\n",ans);
	return 0;
}

NO.2 家務活

題目描述

農場主約翰家人有\(N (3\le N\le 10,000)\)件家務活需要完成,完成第\(i\)件家務活需要\(T_i(1\le T_i\le 100)\)的時間,在做第\(i\)件家務活之前約翰必須完成若干個家務活,我們稱這些家務為\(i\)的必備家務。至少有一個家務沒有必備家務,第一件家務沒有必備家務。

約翰已經安排好完成家務活的順序,家務活\(k\)的必備家務活只會出現在區間\([1,k-1]\)之間。沒有依賴關係的家務活可以同時進行。

現在請你計算約翰家人完成所有家務的最短時間。

輸入格式

第一行為一個整數\(N\),表示有\(N\)件家務活。

接下來\(2~n+1\)行,第\(i+1\)行前兩個數分別為\(T_i\)\(k_i\),表示完成第\(i\)件家務需要\(T_i\)的時間,有\(k_i\)個必備家務,接著\(k\)個數表示第\(i\)件家務的必備家務。

輸出格式

只有一行,約翰完成所有家務的最短時間。

樣例

樣例輸入

7
5 0
1 1 1
3 1 2
6 1 1
1 2 2 4
8 2 2 4
4 3 3 5 6

樣例輸出

23

資料範圍與提示

\(1\): \(0\) 時刻開始,\(5\) 時刻結束

\(2\): \(5\) 時刻開始,\(6\) 時刻結束

\(3\): \(6\) 時刻開始,\(9\) 時刻結束

\(4\): \(5\) 時刻開始,\(11\)時刻結束

\(5\): \(11\)時刻開始,\(12\)時刻結束

\(6\): \(11\)時刻開始,\(19\)時刻結束

\(7\): \(19\)時刻開始,\(23\)時刻結束

分析

看到題目中有約束條件,也就是先到某個點才能到下一個點,所以我們就可以用拓撲排序來排出來工作的先後順序,每一項從這個工作拓展出來的工作是可以同時進行的,所以我們記錄一下同時工作的話花費時間的最大值,雖然讓求的是最小值,但是如果沒做完這個工作是不行的。這樣就處理出來了每個工作到什麼時刻結束。最後當沒有出度,也就是全部工作都應該做完了,那麼就統計答案。

有一個需要注意的地方,因為可能有很多不需要必備家務活的家務,而一開始我們就需要從這些點開始進行拓撲排序,所以我們建一個超級源點,連線入度為\(0\)的點,然後從這個點開始拓撲排序就好了。

程式碼



#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
struct Node{
	int v,next;
}e[5000005];
int n;
int t[maxn],tot,val[maxn];
int head[maxn],ans;
queue<int>q;
int rd[maxn],cd[maxn];
void Add(int x,int y){
	e[++tot].v = y;
	e[tot].next = head[x];
	head[x] = tot;
}
int main(){
	scanf("%d",&n);
	int gs;
	for(int i=1;i<=n;++i){
		scanf("%d%d",&t[i],&gs);
		rd[i] = gs;
		if(!gs)Add(n+1,i);//沒有入度就從超級源點建邊
		val[i] = t[i];//記錄下來每個的時間
		for(int k=1;k<=gs;++k){
			int x;
			scanf("%d",&x);
			Add(x,i);
			cd[x]++;//計算出度
		}
	}
	q.push(n+1);//超級源點開始
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(int i=head[u];i;i=e[i].next){
			int v = e[i].v;
			if(rd[v])rd[v]--;//上個工作做了,入度減一
			val[v] = max(val[v],val[u]+t[v]);//把能夠同時做的工作全做了,找到做完後的時間點
			if(!cd[v])ans = max(ans,val[v]);//沒有出度就統計答案
			if(!rd[v])q.push(v);//沒有入度了說明可以做這個家務,入隊
		}
	}
	printf("%d\n",ans);
	return 0;
}

NO.3 傳紙條

原型:方格取數

題目描述

小淵和小軒是好朋友也是同班同學,他們在一起總有談不完的話題。一次素質拓展活動中,班上同學被安排坐成一個\(m\)行、\(n\)列的矩陣,而小淵和小軒被安排坐在矩陣對角線的兩端,因此,他們就無法直接交談了。幸運的是,他們可以通過傳紙條來進行交流。紙條要經由許多同學傳到對方手裡,小淵坐在矩陣的左上角,座標\((1,1)\),小軒坐在矩陣的右下角,座標\((m,n)\)。從小淵傳給小軒的紙條只可以向下或者向右傳遞,從小軒傳給小淵的紙條只可以向上或者向左傳遞。

在活動進行中,小淵希望給小軒傳一張紙條,同時希望小軒給他回覆。班裡的每個同學都可以幫他們傳遞,但只會幫他們一次,也就是說如果此人在小淵遞給小軒紙條的時候幫忙,那麼在小軒遞給小淵的時候就不會再幫忙。反之亦然。

還有一件事情需要注意,全班每個同學願意幫忙的好心程度有高有低(注意:小淵和小軒的好心程度沒有定義,輸入時用\(0\)表示),可以用一個 \(0~100\)的自然數來表示,數越大表示越好心。小淵和小軒希望儘可能找好心程度高的同學來幫忙傳紙條,即找到來回兩條傳遞路徑,使得這兩條路徑上同學的好心程度之和最大。現在,請你幫助小淵和小軒找到這樣兩條路徑。

輸入格式

第一行有\(2\)個用空格隔開的整數\(m\)\(n\),表示班裡有\(m\)\(n\)\((1\le m,n\le 50)\)

接下來的\(m\)行是一個\(m\times n\)的矩陣,矩陣中第\(i\)\(j\)列的整數表示坐在第\(i\)\(j\)列的學生的好心程度。每行的\(n\)個整數之間用空格隔開。

輸出格式

共一行,包含一個整數,表示來回兩條路上參與傳紙條的同學的好心程度之和的最大值。

樣例

樣例輸入

3 3
0 3 9
2 8 5
5 7 0

樣例輸出

34

資料範圍與提示

\(30\%\)的資料滿足:\(l\le m,n\le 10\)

\(100\%\)的資料滿足:\(1\le m,n\le 50\)

分析

因為從左上到右下要找到兩條路徑,還不能重合,所以我們直接從左上到右下,開四維陣列,前兩個和後兩個分別從上一行、上一列進行轉移,然後取最大值並且加上到的那個點的值,如果算重了就減去,最後輸出就行。下邊說一下具體的狀態:

定義\(f[i][j][k][l]\)為一條路徑到\(i\)\(j\)列,另一條到\(k\)\(l\)列得到的最大值,狀態轉移如下:

\[f[i][j][k][l] = max(max(f[i-1][j][k-1][l],f[i-1][j][k][l-1]),max(f[i][j-1][k-1][l],f[i][j-1][k][l-1]))+a[i][j]+a[k][l]; \]

如果算重了,減去就行。

程式碼



#include<bits/stdc++.h>
using namespace std;
const int maxn = 55;
int f[maxn][maxn][maxn][maxn];
int n,m;
int a[maxn][maxn];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			scanf("%d",&a[i][j]);
		}
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j){
			for(int k=1;k<=n;++k){
				for(int l=1;l<=m;++l){
					f[i][j][k][l] = max(max(f[i-1][j][k-1][l],f[i-1][j][k][l-1]),max(f[i][j-1][k-1][l],f[i][j-1][k][l-1]))+a[i][j]+a[k][l];
					if(i == k && j == l)f[i][j][k][l] -= a[i][j];//算重了就減去
				}
			}
		}
	}
	printf("%d\n",f[n][m][n][m]);
	return 0;
}

NO.4 殺人遊戲

題目描述

一位冷血的殺手潛入 \(Na-wiat\),並假裝成平民。警察希望能在 \(N\) 個人裡面查出誰是殺手。

警察能夠對每一個人進行查證,假如查證的物件是平民,他會告訴警察,他認識的人,誰是殺手,誰是平民。假如查證的物件是殺手,殺手將會把警察幹掉。

現在警察掌握了每一個人認識誰。每一個人都有可能是殺手,可看作他們是殺手的概率是相同的。

問:根據最優的情況,保證警察自身安全並知道誰是殺手的概率最大是多少?

輸入格式

第一行有兩個整數 \(N,M\)

接下來有 \(M\) 行,每行兩個整數 \(x,y\),表示 \(x\) 認識 \(y\)\(y\) 不一定認識 \(x\),例如\(HJT\)同志) 。

輸出格式

僅包含一行一個實數,保留小數點後面 \(6\) 位,表示最大概率。

樣例

樣例輸入#1

5 4
1 2
1 3
1 4
1 5

樣例輸出#1

0.800000

樣例輸入#2

4 2
1 2
2 3

樣例輸出#2

0.750000

樣例輸入#3

7 6
4 1
5 4
2 1
7 3
1 6
2 5

樣例輸出#3

0.714286

資料範圍與提示

警察只需要查證 \(1\)。假如\(1\)是殺手,警察就會被殺。假如 \(1\)不是殺手,他會告訴警察 \(2,3,4,5\) 誰是殺手。而 \(1\) 是殺手的概率是 \(0.2\),所以能知道誰是殺手但沒被殺的概率是\(0.8\)

對於 \(100\%\)的資料有 \(1\le N\le 100000\),\(0\le M\le 300000\)

分析

看到題想到縮點,因為縮完點後,問一個人就能知道一堆人的的資訊,並且每個縮完後的點又相互之間有邊,那麼我們需要找一個入度為\(0\)的點,每有一個就讓\(ans++\),但是縮點後肯定不只是有環,還會有單個的點,像這樣的入度為\(0\),單個點,且他連線的那個點入度不為\(1\),也就是說可以通過其他路徑找出這個人的身份,那麼就標記,讓\(ans--\)。但是這樣的點只允許有一個,因為如果多了,在問完並知道所有人的身份後,還需要問這些孤立的點,那麼不被殺的機率就小了一點,所以只讓最後剩下一個這樣的點,或者沒有。而我們需要求的就是縮點後入度為\(0\)的點的個數。

程式碼

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
struct Node{
	int v,next;
}e[300010<<1],ec[300010<<1];
int head[maxn],tot;
int dfn[maxn],vis[maxn],low[maxn],c[maxn];
int hc[maxn],tc;
int visx[maxn];
int sta[maxn],top,cnt,rd[maxn],siz[maxn];
int num;
int ans;
void Add(int x,int y){
	e[++tot].v = y;
	e[tot].next = head[x];
	head[x] = tot;
}
void Add2(int x,int y){
	ec[++tc].v = y;
	ec[tc].next = hc[x];
	hc[x] = tc;
}
void Tarjan(int x){//縮點
	dfn[x] = low[x] = ++num;
	vis[x] = 1;
	sta[++top] = x;
	for(int i=head[x];i;i=e[i].next){
		int v = e[i].v;
		if(!dfn[v]){
			Tarjan(v);
			low[x] = min(low[x],low[v]);
		}
		else if(vis[v]){
			low[x] = min(low[x],dfn[v]);
		}
	}
	if(dfn[x] == low[x]){
		cnt++;
		int y;
		while(1){
			y=sta[top--];
			c[y]=cnt;
                        siz[cnt]++;
			vis[y] = 0;
			if(x==y)break;
		}
	}
}
bool judge(int x){//判斷是不是他的出邊能夠通過其他點進來而得到他的身份,如果不行就是false
    for(int i=hc[x];i;i=ec[i].next){
        int v = ec[i].v;
        if(rd[v] == 1)return false;
    }
    return true;
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;++i){
		int x,y;
		scanf("%d%d",&x,&y);
		Add(x,y);
	}
	for(int i=1;i<=n;++i){//Tarjan縮點
		if(!dfn[i])Tarjan(i);
	}
	for(int x=1;x<=n;++x){
        for(int i=head[x];i;i=e[i].next){//縮點後新建圖
            int v = e[i].v;
            if(c[x] != c[v]){
                rd[c[v]]++;
                Add2(c[x],c[v]);
            }
        }
    }
    int ans = 0;
    bool flag = 0;
    for(int i=1;i<=cnt;++i){
        if(!flag && siz[i] == 1 && rd[i] == 0 && judge(i)){//標記孤立點
            flag = 1;
        }
        if(rd[i] == 0){//記錄入度為0的點個數
            ans++;
        }
    }
    if(flag)ans--;//如果有孤立的點就減一
    printf("%.6lf",1.0-(double)ans/(double)n);
	
}