【題解】Educational Codeforces Round 82

linyihdfj 發表於 2022-07-02

比較菜只有 A ~ E

A.Erasing Zeroes

題目描述:

原題面

題目分析:

使得所有的 \(1\) 連續也就是所有的 \(1\) 中間\(0\) 全部去掉,也就是可以理解為第一個 \(1\) 到最後一個 \(1\) 中間的 \(0\) 全部去掉,也就是它們之間 \(0\) 的個數,那麼就順序、逆序掃一遍就出來了。

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
int main(){
	int t;
	cin>>t;
	while(t--){
		string s;
		cin>>s;
		int l = -1,r = -1;
		for(int i=0; i<s.size(); i++){
			if(s[i] == '1'){
				l = i;
				break;
			}
		}
		for(int i=s.size(); i>=0; i--){
			if(s[i] == '1'){
				r = i;
				break;
			}
		}
		if(l == -1){
			printf("0\n");
			continue;
		}
		int ans = 0;
		for(int i=l; i<=r; i++){
			if(s[i] == '0'){
				ans++;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

B.National Project

題目描述:

原題面

題目分析:

題目要求是至少有一半是在好天氣中修的,那麼我們就考慮如果好天氣都修那麼修到哪一天可以滿足這個條件。
我們可以將每 \(g\) 天視為一輪,那麼總共就有 \(\lfloor\dfrac{\lceil \dfrac{n}{2} \rceil}{g}\rfloor\) 個完整的輪數,以及多出來的 \(\lceil \dfrac{n}{2} \rceil\% g\) 天。這一輪既有好天氣也有壞天氣,所以最後得到在哪一天可以滿足條件時要算上壞天氣的天數。如果多出來的天數不為 \(0\),那麼就意味著這麼多輪壞天氣都需要經過,而如果多出來的天數為 \(0\),那麼意味著經過壞天氣的輪數是我們求出來的輪數減一,因為最後一輪不需要經過壞天氣。
所以最後在哪一天可以滿足條件的答案就是:(令 \(x = \lfloor\dfrac{\lceil \dfrac{n}{2} \rceil}{g}\rfloor\)

  1. 若有多餘的天數:\(x \times (g+b) + \lceil \dfrac{n}{2} \rceil\% g\)
  2. 若沒有多餘的天數:\((x-1)\times b + x \times g\)
    需要注意的是這幾天只是滿足好天氣一半的條件,不一定可以全部修完,所以需要與 \(n\)\(\max\)

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
int main(){
	long long t;
	cin>>t;
	while(t--){
		long long n,g,b;
		cin>>n>>g>>b;
		long long x = (n+1)/2/g;
		if(((n+1)/2)%g == 0){
			printf("%lld\n",max((x-1) * b + x * g,n));
		}
		else{
			printf("%lld\n",max(x * (g+b) + ((n+1)/2) % g,n));
		}
	}
	return 0;
}

一個優美的技巧:\(\lceil \dfrac{n}{2} \rceil\) 可以寫為: \(\lfloor \dfrac{n+1}{2} \rfloor\)

C.Perfect Keyboard

題目描述:

原題面

題目分析:

這種題很顯然要先考慮轉化為圖上的問題。
很顯然我們可以在 \(S\) 中相鄰的兩個字元之間連邊,表示這兩個字元必須相鄰。
判斷無解的條件也很明顯了:

  1. 某個點的度數大於等於 \(3\),因為一條連邊表示一個相鄰關係,不可能同時與三個字元相鄰。
  2. 出現大於等於 \(3\) 的環,這個也是相當顯然的自己手推一下就知道不可能
    那麼剩下的就是一條條的鏈了,那麼就按順序掃過這一條鏈,到了某個節點就輸出這個節點代表的字母就好了,為了避免重複輸出應該記一個陣列表示這個字母有沒有輸出過。也需要注意一點:要從度數為 \(1\) 的點開始掃,因為度數為 \(2\) 意味著一種中間的關係,顯然只能一邊邊地輸出無法從中間擴充套件。

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 50;
int head[MAXN],cnt,du[MAXN],edge[MAXN][MAXN];
bool vis[MAXN],flag,use[MAXN];
void dfs(int now,int from){
	vis[now] = true;
	for(int i=0; i<26; i++){
		if(i == from || !edge[now][i] || now == i) continue;
		if(vis[i]){
			flag = true;
			return;
		}
		dfs(i,now);
	}
}
void get_ans(int now,int from){
	if(!vis[now])
		cout<<char(now + 'a');
	vis[now] = true;
	for(int i=0; i<26; i++){
		if(vis[i] || i == from || !edge[now][i] || now == i)	continue;
		get_ans(i,now);
	}
}
int main(){
	int t;
	cin>>t;
	while(t--){
		memset(edge,0,sizeof(edge));
		memset(vis,false,sizeof(vis));
		memset(use,false,sizeof(use));
		memset(du,0,sizeof(du));
		cnt = 0;
		flag = false;
		int mx = 0;
		string s;
		cin>>s;
		for(int i=0; i<s.size() - 1; i++){
			if(!edge[s[i]-'a'][s[i+1]-'a']){
				du[s[i]-'a']++;
				du[s[i+1]-'a']++;
				mx = max(mx,max(du[s[i]-'a'],du[s[i+1]-'a']));
			}
			edge[s[i]-'a'][s[i+1]-'a'] = true;
			edge[s[i+1]-'a'][s[i]-'a'] = true;
			use[s[i] - 'a'] = true;
			use[s[i+1] - 'a'] = true;
		}
		for(int i=0; i<26; i++){
			if(!vis[i]){
				dfs(i,i);
			}
			if(flag)
				break;
		}
		if(flag || mx >= 3){
			printf("NO\n");
		}
		else{
			printf("YES\n");
			memset(vis,false,sizeof(vis));
			for(int i=0; i<26; i++){
				if(du[i] == 1)
					get_ans(i,i);
			}
			for(int i=0; i<26; i++){
				if(!vis[i])
					cout<<char(i + 'a');
			}
			printf("\n");
		}
	}
	return 0;
}

因為可能含有大量的重邊而且點的數量很少所以可以考慮使用鄰接矩陣。
輸出的時候也要注意可能有的點沒有出現那麼就在最後輸出這些沒有出現的點。
所謂有大小大於等於三的環也可以理解為無向圖上的有環,也就是如果從當前節點可以到達一個曾經訪問過但不是其父親的點那麼就意味著有環。

D.Fill The Bag

題目描述:

原題面

題目分析:

考慮物品大小都是 \(2\) 的非負整數次冪也就可以聯想到將 \(n\) 二進位制拆分,因為這些物品的順序不影響答案所以就考慮將他們存放到 \(cnt\) 陣列中去。
考慮如果 \(n\) 的當前位我們陣列中有那麼直接減就好了,很顯然這樣做最優。而如果沒有那麼就要考慮是用比當前位小的那些數加和湊出這個位還是從比當前位更大的位拆。
那麼這樣就意味著我們要維護一個 \(sum\) 代表比當前位小的那些數的和,如果這個值大於當前位的值顯然就掃一遍比它小的位然後減去這些位的值直到減為 \(0\) 就好。
而如果用比它小的數湊不出來當前位那麼就找到比當前位大的最小的有值一位,然後從這一位一直拆,拆到當前位然後減掉就好了。

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const long long MAXN = 3e5+5;
long long cnt[MAXN];
int main(){
	long long t;
	cin>>t;
	while(t--){
		memset(cnt,0,sizeof(cnt));
		long long flag = 0;
		long long n,m;
		long long mx = 0;
		cin>>n>>m;
		for(long long i=1; i<=m; i++){
			long long a;
			cin>>a;
			cnt[int(log2(a))]++;
			mx = max(mx,a);
			flag += a;
		}
		if(flag < n){
			printf("-1\n");
			continue;
		}
		long long sum = 0;
		long long now = 0;
		long long ans = 0;
		while(n){
			if(n & 1){
				if(cnt[now]){
					cnt[now]--;
				}
				else if(sum >= (1<<now)){  //用低位補 
					long long res = (1<<now);
					for(long long i=now-1; i>=0 && res; i--){  
						if(cnt[i] * (1<<i) <= res){
							sum -= cnt[i] * (1<<i);
							res -= cnt[i] * (1<<i);
							cnt[i] = 0;
						}
						else{
							while(cnt[i] && (1<<i) <= res){
								res -= (1<<i);
								sum -= (1<<i);
								cnt[i]--;
							}
						}
					}
				}
				else{  //找到高位拆
					for(long long j = now+1; j<=mx; j++){
						if(cnt[j]){
							for(long long k=j; k>now; k--){
								cnt[k-1] += 2;
								cnt[k]--;
								ans++;
							}
							break;
						}
					}
					cnt[now]--;
				}
			}
			n>>=1;
			sum += cnt[now] * (1<<now);
			now++;
		}
		cout<<ans<<endl;
	}
	return 0;
}

注意一開始先判斷有無解,也就是所有數加起來能不能大於等於 \(n\),若能大於等於顯然有解。

E.Erase Subsequences

題目描述:

原題面

題目分析:

我們很明顯可以想出來一步:列舉 \(t\) 在哪裡拆開,然後將 \(t\) 轉化為 \(t1+t2\),再判斷 \(s\) 中能不能拆出 \(t1,t2\) 就好了。
那麼問題就轉化為了 \(s\) 中能不能拆出來的問題了。發現可能需要 \(dp\) 求解。
很顯然的狀態是:\(dp[i][j][k]\) 表示 \(s\) 的前 \(i\) 位能不能拆出 \(t1\) 的前 \(j\) 位和 \(t2\) 的前 \(k\) 位,因為狀態量過大我們就考慮優化這個狀態。
也就是將狀態改寫為:\(dp[i][j]\) 表示 \(s\) 的前 \(i\) 位拆出 \(t1\) 的前 \(j\) 位最多再拆出 \(t2\) 的前多少位。這樣當 \(dp[size_s][size_{t1}] = size_{t2}\) 時也就意味著可以拆出。
那麼就考慮轉移,轉移也就是做決策,這裡的決策顯然就是 \(s\) 的當前位應該放到 \(t1\) 的後面還是 \(t2\) 的後面還是都不放,這樣也能保證是不相交的子序列。
(1)當 \(s[i+1] = t1[j+1]\) 時,\(dp[i][j] \to dp[i+1][j+1]\)
(2)當 \(s[i+1] = t2[f[i][j] + 1]\) 時,\(dp[i][j] + 1 \to dp[i+1][j]\)
(3)任何情況下,\(dp[i][j] \to dp[i+1][j]\),這個也十分顯然

程式碼詳解:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 805;
int f[MAXN][MAXN];
bool check(string s,string t1,string t2){
	memset(f,-1,sizeof(f));
	f[0][0] = 0;
	s = '0' + s;
	t1 = '0' + t1;
	t2 = '0' + t2;
	for(int i=0; i<s.size(); i++){
		for(int j=0; j<t1.size(); j++){
			if(f[i][j] == -1)	continue;
			f[i+1][j] = max(f[i+1][j],f[i][j]);
			if(s[i+1] == t1[j+1])	f[i+1][j+1] = max(f[i+1][j+1],f[i][j]);
			if(s[i+1] == t2[f[i][j] + 1])	f[i+1][j] = max(f[i+1][j],f[i][j] + 1); 
		}
	}
	if(f[s.size()-1][t1.size()-1] == t2.size()-1)
		return true;
	return false; 
}
bool solve(string s,string t){
	for(int i=0; i<=t.size(); i++){
		string t1,t2;
		for(int j=0; j<i; j++){
			t1 = t1 + t[j];
		}
		for(int j=i; j<t.size(); j++){
			t2 = t2 + t[j];
		}
		if(check(s,t1,t2))
			return true;	
	}
	return false;
}
int main(){
	int n;
	cin>>n;
	while(n--){
		string s,t;
		cin>>s>>t;
		if(solve(s,t)){
			printf("YES\n");
		}
		else{
			printf("NO\n");
		}
	}
	return 0;
}

我們會發現對於 \(dp\) 的邊界、初值不好設定,那麼就將所有的字串前面加上一位那就好轉移了。