DAY3-補題

卡布叻-空白發表於2024-10-03

一題之計在於補吶

補題很快樂的一點就是看不懂且聽不明白但是寫註釋中理解到了。

果然學會一道題最簡單的方式是給一個純萌新講。

說在前面

今天%你賽手感非常好,可能是換了一個位置的原因(玄

首先T1沒有讀錯題是一個比較大的進步,因為DAY1和2都是因為差不多這個原因寄掉了,讀對題目果然可以A掉還不錯。T2做題的時候沒有想到正解就很逆天,賽後重構發現第一遍敲程式碼時有一部分思想重合了但沒發現可以這麼做(因為zj是直接找到 pos 的位置,但我的程式碼是按照正解又模擬的(屑

T3賽時一眼合併果子弱化版的強化版(指不需要桶排但一次可以合併 m 個。賽後發現要考慮直接合合不了的情況。T4搓了一個部分分就跑路了,賽後又發現加個判斷可以多拿三十(今天還是很屑。

很屑的是有一道題直接輸出YES有70pts,總司令又又又可以了。

所以掛掉(指可以拿到的但沒有拿到)居然達到了驚人的90分。資料加強謝謝。

最後得分:100+20+10+10 = 140,個人覺得屬於不怎麼好但比前幾天好的情況了。

但今天沒有彩蛋也沒有1145141919810就很不好

T1-IP地址(ip)

賽時敲出正解。

模擬即可。當時 map 忘掉怎麼寫了,所以手搓結構體,本來覺得會爆掉時間複雜度,但一看範圍 \(10^5\) 快樂A。

笑點解析:老師說要 scanf,寫了之後好像有同學掛掉了。

銳評:不如手敲讀優。

歪解:

#include <iostream>
#include <map>
using namespace std;
struct Node {
	string s,s1;
}a[1005];
void read(long long &x){ 
	int f = 1;
	x = 0;
	char s = getchar();
	while(s < '0' || s > '9') {
		if(s == '-') f = -1;
		s = getchar();
	}
	while(s >= '0' && s <= '9') {
		x = x * 10 + s - '0';
		s=getchar();
	}
	x *= f;
}
int main() {
//	freopen("ip.in","r",stdin);
//	freopen("ip.out","w",stdout);
	cin.tie();
	cout.tie();
	long long n;
	read(n);
	for(int i = 1; i <= n; i ++) {
		cin >> a[i].s >> a[i].s1;
	} 
	int q;
	cin >> q;
	for(int i = 1; i <= q; i ++) {
		string s2;
		cin >> s2;
		for(int j = 1; j <= n; j ++) {
			if(a[j].s1 == s2) cout << a[j].s << endl;
		}
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
} 

正解改成 map 即可。

T2 - 是否同構(same)

賽時敲到20。

TLE只能說明我的演算法不夠優啊,列舉k本來就是一個不怎麼高明的方法,只能說當時指向正解的指標可能歪掉了。

放一下程式碼:

#include <iostream>
#define int long long 
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN], b[MAXN];
void read(long long &x){ 
	int f = 1;
	x = 0;
	char s = getchar();
	while(s < '0' || s > '9') {
		if(s == '-') f = -1;
		s = getchar();
	}
	while(s >= '0' && s <= '9') {
		x = x * 10 + s - '0';
		s=getchar();
	}
	x *= f;
}
signed main() {
//	freopen("same.in","r",stdin);
//	freopen("same.out","w",stdout);
	int t;
	cin >> t;
	while(t --) {
		int n;
		cin >> n;
		for(int i = 1; i <= n; i ++) cin >> a[i];
		for(int i = 1; i <= n; i ++) cin >> b[i];
		int flag2 = 0;
		for(int i = 1; i <= n / 2; i ++) { // 列舉k 
		    int flag = 0;
			for(int j = 1; j <= n - i; j ++) {
				swap(a[j], a[n - i + 1]);
			} 
			for(int i = 1; i <= n; i ++) {
				if(a[i] != b[i]) {
					flag = 1;
					break;
				}
			}
			if(flag == 0) {
				cout << "No\n";
				flag2 = 1;
				break;
			}
		}
		if(flag2 == 0) cout << "Yes\n";
	}
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}
//3
//3
//1 2 3
//3 2 1
//4
//3 1 2 4
//4 3 1 2
//5
//2 3 1 4 5
//5 3 1 4 2

正解是很好想的辦法,在 \(a\) 陣列裡找 \(b_{1}\) 出現的位置,然後記錄下來,也就是 pos

接著安照題意 swap 即可。

正解:

#include <iostream>
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 10;
int a[MAXN], b[MAXN], n;
bool check() {
    for(int i = 1; i <= n; i ++) if(a[i] != b[i]) return 0;
    return 1;
}
int main() {
    int t;
    cin >> t;
    while(t --) {
        cin >> n;
        for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
        for(int i = 1; i <= n; i ++) scanf("%d", &b[i]);
        int flag = 0;
        if(check()) {
            printf("Yes\n");
            flag = 1;
            continue;
        }
        int pos = 0;
        for(int i = n - (n / 2) + 1; i <= n; i ++) {
            if(a[i] == b[1]) {
                pos = i;
                break;
            }
        }
        for(int i = pos; i <= n; i ++) {
            swap(a[i], a[i - pos + 1]);
        }
        if(check()) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

T3-箱子(box)

賽時敲掉樣例。

但樣例過水以至於沒有發現需要補0,補上0之後分數果真補0了。

優先佇列自動排序不用管,每次選最小(也就是貪心就好),因為第一次選的肯定是最多次的,所以越小越好,這就是這道題的思路。

很好的是貪心是可以的,我們可以試圖證明一下。

貪心思路:每次選最小的裝進去。

下面思路來自於this

證明不斷取最小的兩堆合併成較大的一堆是最優的。

最優方案可以表示成一個二叉樹。
\(總代價 \sum_{i=1}^{n} a_i × depth_i∑ i=1 \)其中 \(depth\) 是深度,也就是這堆果子在整個過程中被有效合併了幾次。

注意:\(a_i\) 都是葉子結點。非葉子結點都是合併後的產物。

②最小的兩堆一定在最優方案樹的最深層。

這個用反證法。假設有一個最優方案樹,其最深層中沒有最小的兩堆。那麼把最小的堆與最深層的某堆互換位置形成新方案,保證新方案存在而且新方案的代價小於原方案。

注意:最深層總是一組(一組有兩個)或多組葉子節點,來表示它們被直接合並。

③同層葉子節點互換,對總代價無影響。

根據①的 \(\sum\) 得。可見,最小的兩堆,如果在最優方案樹最深層中不是即將合併的一組,那麼可以無償換為一組。

④根據上兩步,我們已經明確:最優方案需要直接合並當前最小的兩堆。現在我們就進行這個操作。事實是:現在只剩 \(n-1\) 堆了。

我們只知道剛才進行了一個絕對不錯的操作,而不記得操作之前是什麼樣的。我們只想對現在剩下的幾堆陌生的果子進行最好的操作。忽略之前的樹,於是回到①了。

注意:這並不意味著之前一步的操作使日後的代價和非常優秀。

顯然的,延伸到取 \(m\) 個也是同樣的思路。

大佬思路真的很清晰啊啊啊。(土撥鼠嚎叫

所以說是 https://www.luogu.com.cn/problem/P1090 的加強版也沒有什麼問題對不對。

正解思路:

觀察到每次把 \(m\) 個合併成 \(1\) 個相當於減去了 \(m - 1\) 個,然後我們可以分兩種情況討論:

  • \((n-1)\space mod\space (m - 1) = 0\) 這種情況我們每次取 個合併最終可以剛好合併成一個,然後我們使用一個小根堆,每次取最小的 \(m\) 個合併即可。

  • \((n-1)\space mod\space (m - 1) ≠ 0\) 這種情況一定會發生一次當前剩餘數量不夠 \(m\) 個的情況,那麼我們可以補足若干個 \(0\),使得滿足\((n-1)\space mod\space (m - 1) = 0\),然後我們按照上述方法做即可,此方法等價於在第一次合併的時候少選幾個箱子,以便讓後面的合併可以每次都取 \(m\) 個。

扔下正解:

#include <bits/stdc++.h>
using namespace std;
long long n,m,x,ans;
priority_queue<long long,vector<long long>,greater<long long> >q;
void read(long long &x){ 
	int f = 1;
	x = 0;
	char s = getchar();
	while(s < '0' || s > '9') {
		if(s == '-') f = -1;
		s = getchar();
	}
	while(s >= '0' && s <= '9') {
		x = x * 10 + s - '0';
		s=getchar();
	}
	x *= f;
}
int main(){
//	freopen("box.in","r",stdin);
//	freopen("box.out","w",stdout);
	read(n);
	read(m);
	for(int i = 1; i <= n; i ++) {
		read(x);
		q.push(x);
	}
	if((n - 1) % (m - 1) > 0) {
		int cnt = m - 1 - (n - 1) % (m - 1);
		while(cnt --) q.push(0);
	}
	while(!q.size()){
		long long cnt = 0;
		bool f = 0;
		for(int i = 1; i <= m; i ++) {
			if(!q.size()) {
			cnt += q.top();
		//	cout << q.top() << " ";
 			q.pop();
			} else {
				f = 1;
				break;
			}
		}
		if(f) break;
		q.push(cnt); 
		ans += cnt;
	}
	printf("%lld",ans);
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

T4-社恐的聚會(party)

行了這道題不會就算了吧。

掙扎一下,眼前一亮,出題人非常良心的給了什麼?!是部分分啊,可是不用動腦子就能拿到的10分啊!

其實再動一下腦子,發現加上一個判斷可以多拿30pts。

最後我們來分析一下正解(因為作者真的蒟蒻不會正解)

我們把所有不互相認識的人之間連一條無向邊,然後我們可以對於所有連通塊判斷當前連通塊是否是二分圖(使用黑白染色法)。

要素過多了哈。

  • 不互相認識的人:指的是我們建圖之後兩個人中間不是雙向的,也就是A認識B但B不認識A,當然也就是我們正解程式碼中的:if(!g[i][j] || !g[j][i]),同樣的,他等效於 !(g[i][j]&&g[j][i])

如果不是二分圖,則直接輸出 NO ,否則我們可以記錄一下每個連通塊中黑點的數量和白點的數量(代表在第一張桌子還是第二張桌子)。注意,黑點和白點本質沒有任何區別,我們每個連通塊都可以黑白互換。然後我們求得每個連通塊的黑點和白點數量後,我們可以做一遍判定性揹包dp來求解答案。

  • 如果不是二分圖,則直接輸出 NO:顯然的偏分技巧。

  • 我們每個連通塊都可以黑白互換:本質上只是一個標記的方法,用什麼顏色都可以的喵。

  • 我們可以做一遍判定性揹包dp來求解答案:這也是這道題最好的地方,因為我看不懂dp本質上是有決策的遞迴(似乎不是來著,反正不是搜尋就是dp)。明顯的,這道題爆搜會炸掉

具體的,設 \(dp_{i,j,0}\) 表示前 \(i\) 個連通塊,是否能塞入 \(j\) 個點到第一張桌子(白點)。設 \(dp_{i,j,1}\) 表示前 \(i\) 個連通塊,是否能塞入 \(j\) 個點到第二張桌子(黑點)。

  • 注意每個連通塊的黑點和白點是可以互換的。

這個解釋非常詳細的說明了思路,同時也沒有想過我們這種xxs聽不懂的問題,不過沒關係,程式碼裡的註釋比較詳細。

dp在老師講完之後倒是比較清晰了。昨天時候講的圖今天完全不會用,果真是蒟蒻了。

本題知識點:二分圖,鏈式前向星,存在性dp,dfs……

總之是一道超綱的題目。放到明天騙分專場倒是不錯。

正解:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 525;
int head[MAXN], nxt[MAXN * MAXN], to[MAXN * MAXN],cnt;
void add(int u, int v) {
    nxt[++ cnt] = head[u];
    to[cnt] = v;
    head[u] = cnt;
} // 構造一個鏈式前向星存一下
// 整篇程式碼唯一簡單易懂的地方(真的簡單易懂哈
int n, g[MAXN][MAXN];
bool vis[MAXN];
int color[MAXN], sz[MAXN][2], idx; // 翻譯一下,size是長度的喵
// idx是目標,Coler是可愛的顏色
// 這就關係到這道題第二個地方——黑白染色(
// 因為看不懂所以可愛到了
bool dfs(int u, int c) { // 是一個dfs,看函式名字就能看出來
    // 主要作用是判斷是否為二分圖
    // 因為只有兩張桌子對不對?所以二分圖(胡說
    vis[u] = true;
    color[u] = c;
    sz[idx][c] ++;
    for(int i = head[u]; i ; i = nxt[i]) {
        int v = to[i];
        if(vis[v]) {
            if(color[u] == color[v]) return 0; // 判斷為假
            else {
                if(!dfs(v, c ^ 1/*相當於取反,因為c不是1就是0*/)) return 0;
            }
        }
    }
    return 1;
}
bool dp[MAXN][MAXN][2]; // 是的你沒有看錯,這裡還有一個dp
// 而且是非常高階的存在性dp,簡稱不會的dp
int main() {
    cin >> n;
    for(int i = 1; i <= n; i ++) {
        for(int j = 1; j <= n; j ++) {
            cin >> g[i][j];
        }
    }
    for(int i = 1; i < n; i ++) {
        for(int j = i + 1; j <= n; j ++) {
            if(!g[i][j] || !g[j][i]) {
                //=>!(g[i][j]&&g[j][i])
                //=>只有相互認識的時候才不會產生連邊,其他的情況都加邊放圖中
                add(i,j);
                add(j,i);
            }
        }
    }
    for(int i = 1; i <= n; i ++) {
        if(vis[i]) continue;
        idx ++;
        if(!dfs(i,0)) {
            cout << "No\n";
            return 0; // 這是這段程式碼最友善的地方,給了部分分且不是多測很好
        }
    }
    dp[0][0][0] = true;
    dp[0][0][1] = true;
    int mx = n / 2;
    for(int i = 1; i <= idx; i ++) {
        for(int j = sz[i][0]; j <= mx; j ++){
            dp[i][j][0] |= dp[i - 1][j - sz[i][0]][0];
            dp[i][j][0] |= dp[i - 1][j - sz[i][0]][1]; 
            // 一個小dp,判斷分在哪個桌子
        }
        for(int j = sz[i][1]; j <= mx; j ++){
            dp[i][j][1] |= dp[i - 1][j - sz[i][1]][0];
            dp[i][j][1] |= dp[i - 1][j - sz[i][1]][1];
        }
    }
    int ans = 0;
    for(int j = mx; j >= 1; j --){
        if(dp[idx][j][0] || dp[idx][j][1]){
            ans = n - j;
            break;
        }
    }
    cout << "Yes" << endl;
    cout << ans << endl;
    return 0;
}

點贊關注喵,點贊關注謝謝喵。