【算法競賽入門經典—訓練指南】學習筆記(含例題代碼與思路)第一章:算法設計基礎


學了一年半$OI$馬上都要退役了,結果居然還沒怎么碰過藍書=_=。這一個月開始刷,力圖把上面的重點都盡可能弄懂。

例題$1$ 勇者斗惡龍(\(UVa11292\)

  • 一眼費用流,再看一眼發現卡不過去。
  • 仔細思考會發現貪心即可。因為騎士能力值和花費是一致的,所以排個序挨個砍,盡可能不把高費騎士浪費在低費頭上即可。
#include <bits/stdc++.h>
using namespace std;

const int N = 20010;

int n, m, A[N], B[N];

int main () {
	while (cin >> n >> m) {
		if (n == 0 && m == 0) break;
		for (int i = 1; i <= n; ++i) cin >> A[i];
		for (int i = 1; i <= m; ++i) cin >> B[i];
		sort (A + 1, A + 1 + n);
		sort (B + 1, B + 1 + m);
		int ans = 0; bool failed = false;
		for (int i = 1, j = 1; i <= n; ++i) {
			while (B[j] < A[i]) ++j;
			if (j > m) {
				failed = true;
				break;	
			}
			if (B[j] >= A[i]) ans += B[j++];
		}
		if (failed) puts ("Loowater is doomed!");
		else cout << ans << endl;
	}
}


例題$2$ 突擊戰(\(UVa11729\)

  • 對於這種要考慮很多條件之間組合的,我們先從兩個開始思考。
    • 建議在紙上自己畫一下兩個任務的執行先后對終止時間的影響。
  • 思考清楚后貪心即可,執行時間長的先交代。
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

int n, _case, sumt1[N];

struct Task {
	int t1, t2;
	bool operator < (const Task &rhs) const {
		return t2 > rhs.t2;
	}
}Arr[N];

int main () {
	while (cin >> n) {
		if (n == 0) break;
		for (int i = 1; i <= n; ++i) cin >> Arr[i].t1 >> Arr[i].t2;
		sort (Arr + 1, Arr + 1 + n);
		for (int i = 1; i <= n; ++i) sumt1[i] = sumt1[i - 1] + Arr[i].t1;
		int ans = 0;
		for (int i = 1; i <= n; ++i) {
			ans = max (ans, sumt1[i] + Arr[i].t2);
		}
		cout << "Case " << ++_case << ": " << ans << endl;
	}
}

例題$3$ 分金幣(\(UVa11300\)

  • 環形均分紙牌問題,需要猜一個結論:一定存在兩個點之間沒有金幣交換。我們枚舉這個點就可以。
  • $O(N^2)->O(N)$的優化:設枚舉點為$k$,把要最小化的答案表示出來,會發現是一個貨倉選址的模型,取中位數即可。
#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 1000010;

int n, A[N], S[N];

signed main () {
	while (cin >> n) {
		int tot = 0, ans = 0;
		for (int i = 1; i <= n; ++i) {cin >> A[i]; tot += A[i];}; // s[i] = \sum{A[i] - Average}
		for (int i = 1; i <= n; ++i) S[i] = S[i - 1] + (A[i] - tot / n);
		sort (S + 1, S + 1 + n);
		for (int i = 1; i <= n; ++i) ans += abs (S[i] - S[n / 2 + 1]); 
		cout << ans << endl; 
	}
}

例題$4$ 墓地雕塑(\(LA3708\)

  • 考慮原題是正$N$邊形變成$N + M$邊形。
  • 兩個結論:
    • 變化前后的兩個正多邊形一定有一個點重合(不妨設其順時針距離為$0$)
    • 其他的點只需要每一個點去尋找一個最近的對應點即可,可以證明不會存在兩個點找同一個點的狀況。
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

int n, m;

double disp, disn;

double dis (int x, int y) {
	return fabs (disp * x - disn * y);
}

int main () {
	while (cin >> n >> m) {
		double ans = 0;
		disp = 10000.0 / (n * 1.0);
		disn = 10000.0 / ((n + m) * 1.0);
		for (int i = 0, p = 0; i < n; ++i) { //對初始點配位 
			while (p + 1 < (n + m) && dis (i, p) > dis (i, p + 1)) {
				p = p + 1;
			}
			ans += dis (i, p); p = p + 1;
		}
		printf ("%.4lf\n", ans);
	}
} 


例題$5$ 螞蟻(\(UVa10881\)

  • 關鍵在於要想到螞蟻的相對位置不變~明白這一點剩下就簡單了。
  • 兩個螞蟻的碰撞可以想象為它們穿過彼此繼續前進但是靈魂互換,這樣我們可以得到坐標和方向正確但是順序不確定的坐標序列,根據上面的結論確定順序編號即可。
#include <bits/stdc++.h>
using namespace std;

const int N = 10010;

int T, l, t, n;

int pos[N], str[N];

struct Ant {
	int id, pos; char dir;
	
	bool operator < (Ant rhs) const { //相對位置不變 
		return pos == rhs.pos ? dir < rhs.dir : pos < rhs.pos;
	}
}Ap[N], An[N];

char res[4][10] = {"L", "R", "Turning", "Fell off"};

int main () {
	cin >> T;
	for (int Case = 1; Case <= T; ++Case) {
		cin >> l >> t >> n;
		for (int i = 1; i <= n; ++i) {
			cin >> Ap[i].pos >> Ap[i].dir;
			Ap[i].id = i;
			An[i].dir = Ap[i].dir;  
			An[i].pos = Ap[i].dir == 'R' ? Ap[i].pos + t : Ap[i].pos - t;
		}
		sort (Ap + 1, Ap + 1 + n);
		sort (An + 1, An + 1 + n);
		for (int i = 1; i <= n; ++i) {
//			printf ("id = %d, pos = %d, dir = %c\n", Ap[i].id, An[i].pos, An[i].dir);
			pos[Ap[i].id] = An[i].pos;
			if (An[i].dir == 'L') str[Ap[i].id] = 0;
			if (An[i].dir == 'R') str[Ap[i].id] = 1;
//			printf ("pos_now = %d, pos_pre = %d\n", An[i].pos, An[i - 1].pos);
			if (i + 1 <= n && An[i].pos == An[i + 1].pos) str[Ap[i].id] = 2;
			if (0 <= i - 1 && An[i].pos == An[i - 1].pos) str[Ap[i].id] = 2;
			if (l < An[i].pos || An[i].pos < 0) str[Ap[i].id] = 3;
		}
		printf ("Case #%d:\n", Case);
		for (int i = 1; i <= n; ++i) {
			if (str[i] == 3) puts (res[3]);
			else {
				printf ("%d %s\n", pos[i], res[str[i]]);
			}
		}
		printf ("\n");
	} 
} 

例題$6$ 立方體成像(\(LA2995\)

  • 每一個小塊如果顏色無法對應就刪刪刪~
  • 關鍵在於確定立方體的每一個塊的每一個面應該怎么表示。用函數式編程的思想來簡化代碼。
#include <bits/stdc++.h>
using namespace std;

const int N = 15;
#define rep(i,n) for (int i = 0; i < (n); ++i)

int n; char pos[N][N][N], view[6][N][N];

void get (int k, int i, int j, int len, int &x, int &y, int &z) {
	if (k == 0) x = len, y = j, z = i;
	if (k == 1) x = n - j - 1, y = len, z = i;
	if (k == 2) x = n - len - 1, y = n - j - 1, z = i;
	if (k == 3) x = j, y = n - len - 1, z = i;
	if (k == 4) x = n - i - 1, y = j, z = len;
	if (k == 5) x = i, y = j, z = n - len - 1;
}

int main () {
	while (cin >> n) {
		if (n == 0) break;
		rep (i, n) rep (k, 6) rep (j, n) scanf (" %c", &view[k][i][j]);
		rep (i, n) rep (j, n) rep (k, n) pos[i][j][k] = '#';
		
		rep (k, 6) rep (i, n) rep (j, n) if (view[k][i][j] == '.') {
			rep (p, n) {
				int x, y, z;
				get (k, i, j, p, x, y, z);
				pos[x][y][z] = '.'; // 清除視圖k下坐標 (i, j) 所有深度的立方體 
			}	
		}
		
		while (true) { // 不能再修改為止 
			bool done = true;
			rep (k, 6) rep (i, n) rep (j, n) if (view[k][i][j] != '.') {
				rep (p, n) {
					int x, y, z;
					get (k, i, j, p, x, y, z);
					if (pos[x][y][z] == '.') continue; // 空 
					if (pos[x][y][z] == '#') { // 暴露在外且未填 
						pos[x][y][z] = view[k][i][j];
						break;
					}
					if (pos[x][y][z] == view[k][i][j]) break; // 暴露在外且顏色並不矛盾 
					pos[x][y][z] = '.'; // 暴露在外且顏色矛盾 -> 刪除並讓下一個深度暴露在外 
					done = false;
				}
			}
			if (done) break;
		}
		
		int ans = 0;
		rep (i, n) rep (j, n) rep (k, n) {
			if (pos[i][j][k] !=  '.') ans++;
		}
		printf ("Maximum weight: %d gram(s)\n", ans);
	}
}

例題$7$ 偶數矩陣(\(UVa11464\)

  • 常見套路:固定第一行以后,后面的行都可以直接$O(N)$遞推,復雜度$O(2N*N2)$
#include <bits/stdc++.h>
using namespace std;

const int N = 20;
const int INF = 1e9;

int T, n, ans, rem[N], mp[N][N], surr[N][N];

int mv[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

bool in_map (int x, int y) {
	return 0 <= x && x < n && 0 <= y && y < n;
}

void add_ele (int x, int y) {
//	printf ("add (%d, %d)\n", x, y);
	for (int k = 0; k < 4; ++k) {
		int tx = x + mv[k][0];
		int ty = y + mv[k][1];
		if (in_map (tx, ty)) {
//			printf ("surr[%d][%d]++\n", tx, ty);
			surr[tx][ty] += 1;
		}
	}
}

int get_ans (int sit, int prew) {
//	cout << "sit = " << sit << endl;
//  sit -> 上一行的狀態
	int ret = 0; 
//	cout << "sit = " << sit << endl;  
	memset (surr, 0, sizeof (surr));
	for (int i = 0; i < n; ++i) {
		if (sit & (1 << i)) add_ele (0, i);
	}
//	for (int i = 0; i < n; ++i) {
//		for (int j = 0; j < n; ++j) {
//			printf ("%d ", surr[i][j]);
//		}
//		printf ("\n");
//	}
	for (int i = 1; i < n; ++i) {
		for (int j = 0; j < n; ++j) {
//			printf ("surr[%d - 1][%d] = %d, mp[%d][%d] = %d\n", i, j, surr[i - 1][j], i, j, mp[i][j]);
			if (mp[i][j] == 1) add_ele (i, j);
			if (surr[i - 1][j] % 2 == 1) {
				if (mp[i][j] == 1) return INF;
				add_ele (i, j);
				ret++;
			}
		}
	}
//	cout << "ret = " << ret << " w = " << prew << endl; 
	return ret + prew;
}

void dfs (int i, int w, int sit) {
	if (i == rem[0] + 1) {
		ans = min (ans, get_ans (sit, w));
		return;
	}
	dfs (i + 1, w + 1, sit | (1 << rem[i]));
	dfs (i + 1, w + 0, sit | (0 << rem[i]));
}

int main () {
//	freopen ("data.in", "r", stdin);
	cin >> T;
	for (int Case = 1; Case <= T; ++Case) {
		ans = INF;
		cin >> n;
		for (int i = 0; i < n; ++i) {
			for (int j = 0; j < n; ++j) {
				cin >> mp[i][j];
			}
		}
		int ini = 0;
		memset (rem, 0, sizeof (rem));
		for (int i = 0; i < n; ++i) {
			if (!mp[0][i]) rem[++rem[0]] = i;
			ini |= (mp[0][i] << i);
		}
		dfs (1, 0, ini);
		cout << "Case " << Case << ": " << (ans == INF ? -1 : ans) << endl;
	} 
} 

例題$8$ 彩色立方體(\(LA3401\)

  • 編寫關鍵在於確定每一個立方體可以旋轉成多少種形式,為每個立方體確定形態。
  • 同樣是函數式編程的思想簡化代碼,建議提前對立方體的姿態映射打表。
#include <bits/stdc++.h>
using namespace std;

int dice24[24][6] = {
	{2, 1, 5, 0, 4, 3}, {2, 0, 1, 4, 5, 3}, {2, 4, 0, 5, 1, 3}, {2, 5, 4, 1, 0, 3},
	{4, 2, 5, 0, 3, 1}, {5, 2, 1, 4, 3, 0}, {1, 2, 0, 5, 3, 4}, {0, 2, 4, 1, 3, 5},
	{0, 1, 2, 3, 4, 5}, {4, 0, 2, 3, 5, 1}, {5, 4, 2, 3, 1, 0}, {1, 5, 2, 3, 0, 4},
	{5, 1, 3, 2, 4, 0}, {1, 0, 3, 2, 5, 4}, {0, 4, 3, 2, 1, 5}, {4, 5, 3, 2, 0, 1},
	{1, 3, 5, 0, 2, 4}, {0, 3, 1, 4, 2, 5}, {4, 3, 0, 5, 2, 1}, {5, 3, 4, 1, 2, 0},
	{3, 4, 5, 0, 1, 2}, {3, 5, 1, 4, 0, 2}, {3, 1, 0, 5, 4, 2}, {3, 0, 4, 1, 5, 2},
};

const int N = 4;

int n, ans, dice[N][6];

vector <string> names;

int ID (const char *name) {
	string s = name;
	int n = names.size ();
	for (int i = 0; i < n; ++i) {
		if (names[i] == s) return i;
	}
	names.push_back (s);
	return n;
}

int r[N], color[N][6];

void check () {
	for (int i = 0; i < n; ++i) {
		for (int j = 0; j < 6; ++j) {
			color[i][dice24[r[i]][j]] = dice[i][j];
		}
	}
	int tot = 0;
	for (int j = 0; j < 6; ++j) {
		int cnt[N * 6];
		memset (cnt, 0, sizeof (cnt));
		int maxface = 0;
		for (int i = 0; i < n; ++i) {
			maxface = max (maxface, ++cnt[color[i][j]]);
		}
		tot += n - maxface;
	} 
	ans = min (ans, tot);
}

void dfs (int d) {
	if (d == n) {check (); return;}
	for (int i = 0; i < 24; ++i) {
		r[d] = i;
		dfs (d + 1);
	}
}

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n) {
		if (n == 0) break;
		names.clear ();
		for (int i = 0; i < n; ++i) {
			for (int j = 0; j < 6; ++j) {
				char name[30];
				scanf ("%s", name);
				dice[i][j] = ID (name);
			}
		}
		ans = n * 6;
		r[0] = 0; dfs (1);
		cout << ans << endl;
	}	
}

例題$9$ 中國麻將(\(UVa11210\)

  • 暴力搜索,類似於斗地主那個題?枚舉出來每一種牌型看能不能枚舉完就好。
#include <bits/stdc++.h>
using namespace std;

string mahjong[34] = {
	"1T", "2T", "3T", "4T", "5T", "6T", "7T", "8T", "9T", 
	"1S", "2S", "3S", "4S", "5S", "6S", "7S", "8S", "9S", 
	"1W", "2W", "3W", "4W", "5W", "6W", "7W", "8W", "9W", 
	"DONG", "NAN", "XI", "BEI", "ZHONG", "FA", "BAI"
};

string s[14]; int bel[14], bin[34];

bool equals (string s1, string s2) {
	if (s1.length () != s2.length ()) return false;
	for (int i = 0; i < (int)s1.length (); ++i) {
		if (s1[i] != s2[i]) return false;
	}
	return true;
}

int id (string str) {
	for (int i = 0; i < 34; ++i) {
		if (equals (str, mahjong[i])) {	
			return i;
		}
	}
	return -1;
}

int Case = 0;

bool judge () {//剩下的是否全是3個
//	printf ("In!");
	for (int i = 0; i < 34; ++i) {
		if (bin[i] != 0 && bin[i] != 3) return false;
	}
	return true;
}

bool dfs (int x, int r) {//判斷順子
//	printf ("x = %d, r = %d\n", x, r);
	if (x == r * 9) {
		if (x == 27) return judge ();
		else return dfs (x, r + 1);
	}
	int ret = false;
	if (x + 2 < r * 9) {
		if (bin[x + 0] && bin[x + 1] && bin[x + 2]) {
			bin[x + 0]--, bin[x + 1]--, bin[x + 2]--;
			ret |= dfs (x, r); 
			bin[x + 0]++, bin[x + 1]++, bin[x + 2]++;
		}
	}
	ret |= dfs (x + 1, r);
	return ret;
}

bool can_use (int x) {
	bin[x]++;
	int ret = 0;
//	cout << endl;
//	for (int i = 0; i < 34; ++i) {
//		if (bin[i]) cout << mahjong[i] << ": " << bin[i] << endl; 
//	}
	for (int i = 0; i < 34; ++i) {
		if (bin[i] >= 2) {
			bin[i] -= 2;
//			printf ("i = %d\n", i); 
			for (int i = 0; i < 34; ++i) {
//				if (bin[i]) cout << mahjong[i] << ": " << bin[i] << endl; 
			}
			ret |= dfs (0, 1);
			bin[i] += 2;
		}
	}//枚舉將牌 
	bin[x]--;
	return ret;
}

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> s[0] && s[0][0] != '0' && ++Case) {
		cout << "Case " << Case << ":";
		memset (bin, 0, sizeof (bin));
		for (int i = 1; i < 13; ++i) cin >> s[i];
		for (int i = 0; i < 13; ++i) bin[bel[i] = id (s[i])]++;
//		printf ("can_use (12) = %d\n", can_use (12));
		bool succeeded = false;
		for (int i = 0; i < 34; ++i) {
			if (bin[i] < 4 && can_use (i)) {
				cout << " " << mahjong[i];
				succeeded = true;
			}
		}
		if (!succeeded) printf (" Not ready");
		cout << endl;
	}
}

例題$10$ 正整數序列(\(UVa11384\)

  • 關鍵在能不能想到兩個$[1,x]\(和一個\)[1,x]$消除的步數是一樣的。
  • 又因為$Ans(x)\(顯然單調遞增,所以把一個\)[1,x]\(拆分成兩個\)[1,ceil(x/2)]$一定是最優的,遞歸求解即可。
#include <bits/stdc++.h>
using namespace std;

int n;

int f (int x) {
	if (x <= 1) return x;
	return f(x / 2) + 1;
}

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n) {
		cout << f(n) << endl;
	}
} 

例題$11$ 新漢諾塔問題(\(UVa10795\)

  • 切入點是把盤子從大到小依次歸位,已經歸位的可以忽略,然后就可以考慮怎么把當前最大的未歸位盤子歸位。由於盤子可以倒着放回去所以過程可逆,這點很關鍵。
  • 詳情參考藍書。$LRJ$講的相當清楚。
#include <bits/stdc++.h>
using namespace std;

#define int long long

int f (int *P, int i, int final) { //f (x, y, z) -> 把狀態x下[1, i]的柱子移動到final上 
	if (i == 0) return 0;
	if (P[i] == final) return f (P, i - 1, final);
	return f (P, i - 1, 6 - P[i] - final) + (1ll << (i - 1));
}

const int N = 65;

int n, start[N], finish[N];

signed main () {
	int Case = 0;
	while (cin >> n && n != 0) {
		for (int i = 1; i <= n; ++i) cin >> start[i];
		for (int i = 1; i <= n; ++i) cin >> finish[i];
		int k = n;
		while (k >= 1 && start[k] == finish[k]) k--;
		
		int ans = 0;
		if (k >= 1) {
			int etc = 6 - start[k] - finish[k];
			ans = f (start, k - 1, etc) + f (finish, k - 1, etc) + 1;
		} 
		cout << "Case " << ++Case << ": " << ans << endl;
	}
}


例題$12$ 組裝電腦(\(LA3971\)

  • 常見套路:最小值最大考慮二分。
  • 個人想法:似乎還可以做一個價格—質量排序,使價格遞增時質量也單調遞增,然后貪心地刪除?不過好像比二分麻煩=。=
  • 二分技巧:把區間划分為可用和不可用兩部分,繼續二分的條件是$l< r$,使$mid$向不可用的那一個方向靠攏。
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;
const int INF = 0x3f3f3f3f;

int T, n, b, tot, pri[N], val[N];

string type[N], name[N];

map <string, int> id;

int cho_pri[N];

bool can_use (int minw) {
	//選中的物件其w要>=minw
	memset (cho_pri, 0x3f, sizeof (cho_pri));
	for (int i = 1; i <= n; ++i) {
		if (val[i] >= minw) {
			cho_pri[id[type[i]]] = min (cho_pri[id[type[i]]], pri[i]);
		}
	}
	int have = b;
	for (int i = 1; i <= tot; ++i) {
		if (cho_pri[i] == INF) return false;
		if (have < cho_pri[i]) return false;
		have -= cho_pri[i];
	}
	return true;
}

int main () {
	cin >> T;
	while (T--) {
		tot = 0;
		id.clear ();
		cin >> n >> b;
		for (int i = 1; i <= n; ++i) {
			cin >> type[i] >> name[i] >> pri[i] >> val[i];
			if (!id.count (type[i])) id[type[i]] = ++tot;
		}
		int l = 0, r = 1e9 + 7;
		while (l < r) {
			int mid = (l + r + 1) >> 1; 
			if (can_use (mid)) {
				l = mid;
			} else {
				r = mid - 1;
			}
		}
		cout << l << endl;
	}
}

例題$13$ 派(\(LA3635\)

  • 求最大面積,實數二分。枚舉對每一個整塊派的划分。
#include <bits/stdc++.h>
using namespace std;

const int N = 10010;
const double eps = 1e-5;
const double pi = acos(-1);

int T, n, m, r[N];

bool can_use (double s) {
	//每個派切出面積s,切出來個數是否>=m;
	int tot = 0;
	for (int i = 1; i <= n; ++i) {
		tot += floor (pi * r[i] * r[i] / s);
	} 
	return tot >= m;
}

int main () {
	cin >> T;
	while (T--) {
		cin >> n >> m; ++m; 
		for (int i = 1; i <= n; ++i) cin >> r[i];
		double l = 0, r = 1e9;
		while (r - l > eps) {
			double mid = (l + r) / 2.0;
			if (can_use (mid)) {
				l = mid;
			} else {
				r = mid;
			}
		}
		printf ("%.4lf\n", l);
	}
}


例題$14$ 填充正方形(\(UVa11520\)

  • 模擬,沒啥難度,填就完事了。
#include <bits/stdc++.h>
using namespace std;

const int N = 15;

int T, n; 
char mp[N][N]; 
bool surr[N][N][27];

int mv[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

bool in_map (int x, int y) {
	return 1 <= x && x <= n && 1 <= y && y <= n;
}

void use (int x, int y, char ch) {
	for (int i = 0; i < 4; ++i) {
		int tx = x + mv[i][0];
		int ty = y + mv[i][1];
		if (in_map (tx, ty)) {
//			printf ("surr (%d, %d, %d) = true\n", tx, ty, ch - 'A');
			surr[tx][ty][ch - 'A'] = true;
		}
	} 
}

int main () {
//	freopen ("data.in", "r", stdin);
//	freopen ("data.out", "w", stdout);
	cin >> T;
	for (int Case = 1; Case <= T; ++Case) {
		cout << "Case " << Case << ":" << endl; 
		cin >> n;
		memset (surr, 0, sizeof (surr));
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j) {
				cin >> mp[i][j];
				if (mp[i][j] != '.') {
					use (i, j, mp[i][j]);
				}
			}
		}
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j) {
				if (mp[i][j] == '.') {
					for (int k = 0; k < 5; ++k) {
						if (!surr[i][j][k]) {
							use (i, j, 'A' + k);
							mp[i][j] = 'A' + k;
							break;
						}
					}
				}
				putchar (mp[i][j]);
			}
			putchar ('\n');
		}
	}
}

例題$15$ 網絡(\(LA3902\)

  • 和$Luogu$上那個消防局的設立是一樣的。。
  • 有根樹轉無根樹,每次取深度最大的葉子節點的$k$級祖先,刪除其周邊節點。正確性顯然。
  • 復雜度$O(NlogN)$(但后來我倍增掛了就變成$O(N^2logN)$了)
  • 注意注意注意要判斷葉子!
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int T, n, s, k, cnt, head[N];

int du[N];

struct edge {
	int nxt, to;
}e[N << 1];

void add_len (int u, int v) {
	e[++cnt] = (edge) {head[u], v}; head[u] = cnt;
	e[++cnt] = (edge) {head[v], u}; head[v] = cnt;
}

struct Node {
	int p, d;
	
	bool operator < (Node rhs) const {
		return d < rhs.d;
	}
};

priority_queue <Node> q;
int deep[N], pre[N];

void dfs (int u, int fa, int d) {
	pre[u] = fa;
	if (d > k && du[u] == 1) {
		q.push ((Node) {u, d});
//		printf ("u = %d, deep[u] = %d\n", u, deep[u]);
	}
	for (int i = head[u]; i; i = e[i].nxt) {
//		printf ("%d -> %d\n", u, e[i].to);
		int v = e[i].to;
		if (v != fa) {
			dfs (v, u, d + 1);
		}
	}
}

bool vis[N];

void take_place (int u, int d, int fa) {
	vis[u] = true;
	for (int i = head[u]; i; i = e[i].nxt) {
		int v = e[i].to;
		if (v != fa && d < k) {
			take_place (v, d + 1, u);
		}
	} 
}

int main () {
//	freopen ("data.in", "r", stdin);
//	freopen ("data.out", "w", stdout);
	cin >> T;
	while (T--) {
		cnt = 0;
		memset (du, 0, sizeof (du));
		memset (vis, 0, sizeof (vis));
		memset (head, 0, sizeof (head));
		cin >> n >> s >> k;
		for (int i = 1; i <= n - 1; ++i) {
			static int u, v;
			cin >> u >> v;
			add_len (u, v);
			du[u]++, du[v]++;
		}
		dfs (s, 0, 0);
		int ans = 0;
		while (!q.empty ()) {
			int u = q.top ().p; q.pop ();
			if (vis[u]) continue;
			ans = ans + 1;
			for (int i = 0; i < k; ++i) u = pre[u];
			take_place (u, 0, 0); 
		}
		cout << ans << endl;
	}
}

例題$16$ 長城守衛(\(LA3177\)

  • 偶數的時候很好辦,奇數的時候就是一個很巧妙的構造思想了。
  • 我們先假設第一個人選了$[1,r_1]$這些禮物,只要讓其他偶數位盡可能靠前,奇數位盡可能靠后,我們$1$和$N$就可以盡可能保證不相交。比較繞的一點在於要記錄第$i$個人選了$[1,r_1]\(的部分有多少,選了\)[r_1+1,N]$的部分有多少。這里我們二分一個最小值,使之滿足第$N$個人不會在$[1,r_1]$部分選擇即為合法。
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
const int INF = 0x3f3f3f3f;

int n, r[N], Ltot[N], Rtot[N];

bool can_use (int tot) {
	int x = r[1], y = tot - r[1];
	Ltot[1] = x, Rtot[1] = 0;
	for (int i = 2; i <= n; ++i) {
		if (i % 2 == 1) {//奇數(往后取) 
			Rtot[i] = min (y - Rtot[i - 1], r[i]);
			Ltot[i] = r[i] - Rtot[i]; 
		} else {
			Ltot[i] = min (x - Ltot[i - 1], r[i]);
			Rtot[i] = r[i] - Ltot[i];
		}
	}
	return Ltot[n] == 0;
}

int main () {
	while (cin >> n && n) {
		for (int i = 1; i <= n; ++i) cin >> r[i];
		if (n == 1) {
			cout << r[1] << endl; 
			continue;
		}
		r[n + 1] = r[1];
		int L = 0, R = INF;
		for (int i = 1; i <= n; ++i) {
			L = max (L, r[i] + r[i + 1]);
		}
		if (n % 2 == 0) {
			cout << L << endl; 
		} else {
			while (L < R) {
				int mid = (L + R) >> 1;
				if (can_use (mid)) {
					R = mid;
				} else {
					L = mid + 1;
				}
			}
			cout << R << endl;
		}
	}
} 

例題$17$ 年齡排序(\(UVa11462\)

  • 桶排序板子,注意性能優化。
#include <bits/stdc++.h>
using namespace std;

int read () {
	int s = 0, w = 1, ch = getchar ();
	while ('9' < ch || ch < '0') {
		s = s * 10 + ch - '0';
		ch = getchar (); 
	}
	while ('0' <= ch && ch <= '9') {
		s = s * 10 + ch - '0';
		ch = getchar ();
	}
	return s * w;
}

int n; short bin[110];

int main () {
	while ((n = read()) != 0) {
		memset (bin, 0, sizeof (bin));
		for (int i = 1; i <= n; ++i) bin[read ()]++;
		bool first = true;
		for (int i = 1; i <= 100; ++i) {
			for (int j = 1; j <= bin[i]; ++j) {
				if (!first) putchar (' ');
				printf ("%d", i);
				first = false;
			}
		}
		printf ("\n");
	}
}

例題$18$ 開放式學分制(\(UVa11078\)

  • 最開始學傻了糊了一個$RMQ$上去,后來才意識到維護一個前綴最大和后綴最小就可以了=_=太懶了代碼就沒換寫法
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
const int INF = 1e9;

int T, n, arr[N];

namespace RMQ {
	int maxw[N][20], minw[N][20];
	
	void Init () {
		for (int i = 1; i <= n; ++i) {
			maxw[i][0] = minw[i][0] = arr[i];
		}
		for (int i = 1; (1 << i) <= n; ++i) {
			for (int j = 1; j + (1 << i) - 1 <= n; ++j) {
				maxw[j][i] = max (maxw[j][i - 1], maxw[j + (1 << (i - 1))][i - 1]);
				minw[j][i] = min (minw[j][i - 1], minw[j + (1 << (i - 1))][i - 1]);
			}
		}
	}
	
	int query_max (int l, int r) {
		int mx = log2 (r - l + 1);
		return max (maxw[l][mx], maxw[r - (1 << mx) + 1][mx]); 
	}
	
	int query_min (int l, int r) {
		int mx = log2 (r - l + 1);
		return min (minw[l][mx], minw[r - (1 << mx) + 1][mx]); 
	}
}

int main () {
	cin >> T;
	while (T--) {
		cin >> n;
		for (int i = 1; i <= n; ++i) cin >> arr[i];
		RMQ :: Init ();
		int ans = -INF;
		for (int i = 1; i < n; ++i) {
			ans = max (ans, RMQ :: query_max (1, i) - RMQ :: query_min (i + 1, n));
		}
		cout << ans << endl;
	}
}

例題$19$ 計算器謎題(\(UVa11549\)

  • $Floyd$判圈算法。形象來說就是只要有圈的話,一個每次跑一步,一個每次跑兩步。在追上的時候,這個圈就被確定存在並且枚舉完了。原題在一個范圍似乎不會太大的剩余系里面,所以一定有圈。復雜度$O(N*k = 能過)(0<k<1)$
#include <bits/stdc++.h>
using namespace std;

int T, n, k; long long _pow[20];

int _nxt (int x) {
	if (x == 0) return 0;
	int wei = log10 (1ll * x * x) + 1; //位數
//	cout << "wei_" << x  << " = " << wei << endl
	return 1ll * x * x / _pow[max (wei - n, 0)]; 
}

int main () {
//	freopen ("data.in", "r", stdin);
	cin >> T;
	_pow[0] = 1;
	for (int i = 1; i <= 18; ++i) {
		_pow[i] = _pow[i - 1] * 10;
	}
	while (T--) {
		cin >> n >> k;
		int k1 = k, k2 = k, ans = k;
		do {
			k1 = _nxt (k1); ans = max (ans, k1);
			k2 = _nxt (k2); ans = max (ans, k2);
			k2 = _nxt (k2); ans = max (ans, k2);
		} while (k1 != k2);
		cout << ans << endl;
	}
}

略困,今晚先更新到這。——$2019$年$04$月$15$日$23:43:37$


例題$20$  流星(\(LA3905\)

  • 算是計算幾何吧。。。實際上挺簡單的。用了掃描的思想。
  • 每一個流星表示成一個初始點和一個速度的向量,實際上最后還是在時間軸上取一段連續的時間覆蓋上去。
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
const double eps = 1e-8;
const int INF = 0x7fffffff;

int T, n, w, h;

double sl[N], sr[N];

struct Element {
	double pos; int val;
	bool operator < (Element rhs) const {
		return pos < rhs.pos;
	}
}ele[N << 1];

int main () {
	cin >> T;
	while (T--) {
		int tot = 0;
		cin >> w >> h >> n;
		for (int i = 1; i <= n; ++i) {
			static int x, y, vx, vy;
			cin >> x >> y >> vx >> vy;
			double lx, rx, ly, ry;
			if (vx > 0) lx = max ((double)(0 - x) / vx, 0.0), rx = max ((double)(w - x) / vx, 0.0);
			if (vx < 0) lx = max ((double)(w - x) / vx, 0.0), rx = max ((double)(0 - x) / vx, 0.0);
			if (vx == 0) {if (0 < x && x < w) lx = 0, rx = INF; else lx = rx = 0;}
			if (vy > 0) ly = max ((double)(0 - y) / vy, 0.0), ry = max ((double)(h - y) / vy, 0.0);
			if (vy < 0) ly = max ((double)(h - y) / vy, 0.0), ry = max ((double)(0 - y) / vy, 0.0);
			if (vy == 0) {if (0 < y && y < h) ly = 0, ry = INF; else ly = ry = 0;}
			double l = max (lx, ly), r = min (rx, ry);
			if (r - l > eps) {
				ele[++tot] = (Element) {l, +1};
				ele[++tot] = (Element) {r, -1};
			}
		}
		int res = 0, ans = 0;
		sort (ele + 1, ele + 1 + tot);
		for (int i = 1; i <= tot; ++i) {	
			res += ele[i].val;
			while (ele[i].pos == ele[i + 1].pos) {
				res += ele[++i].val;
			}
			ans = max (ans, res);
		}
		cout << ans << endl;
	}
}

例題$21$  子序列(\(LA2678\)

  • 題目本身很簡單,厲害的是它的思想。
  • 解法$1$:直接二分
    • 算法顯然。復雜度$O(NlogN)$
  • 解法$2$:考慮枚舉。
    • 最簡單的,可以$O(N3)$枚舉每一個子段,用前綴和可以優化到$O(N2)$
    • 注意到對於每一個$r$,只有一個$l$可能會對答案產生貢獻,而且這個$l$的位置單調遞增(因為是正整數序列),所以枚舉就變成了$O(N)$的,每個$l$最多被作為左端點被掃到一遍。
    • 懶省事就沒特判。。詳見代碼=_=
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n, s, sumw[N];

int read () {
	int s = 0, ch = getchar ();
	while ('9' < ch || ch < '0') {
		ch = getchar ();
	}
	while ('0' <= ch && ch <= '9') {
		s = s * 10 + ch - '0';
		ch = getchar ();
	}
	return s;
}

int main () {
	while (cin >> n >> s) {
		for (int i = 1; i <= n; ++i) sumw[i] = sumw[i - 1] + read ();
		int minlen = n + 1;
		for (int r = 1, l = 1; r <= n; ++r) {
			if (sumw[r] - sumw[l - 1] >= s) {
				while (sumw[r] - sumw[l - 1] >= s) ++l; --l;
				minlen = min (minlen, r - l + 1);
			}
		}
		cout << minlen % (n + 1) << endl;
	}
}


例題$22$ 最大子矩陣(\(LA3029\)

  • 題意:含障礙網格圖,求最大子矩陣。
  • 解法:懸線法,掃描的思想,詳見代碼。
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;

int mat[N][N], up[N][N], ll[N][N], rr[N][N];

int T, m, n;

int main () {
	cin >> T;
	while (T--) {
		cin >> m >> n;
		for (int i = 0; i < m; ++i) {
			for (int j = 0; j < n; ++j) {
				int ch = getchar ();
				while (ch != 'F' && ch != 'R') ch = getchar ();
				mat[i][j] = ch == 'F' ? 0 : 1;
			}
		}
		int ans = 0;
		for (int i = 0; i < m; ++i) {
			int lo = -1, ro = n;
			for (int j = 0; j < n; ++j) {
				if (mat[i][j] == 1) {
					up[i][j] = ll[i][j] = 0; lo = j;
				} else {
					up[i][j] = i == 0 ? 1 : up[i - 1][j] + 1;
					ll[i][j] = i == 0 ? lo + 1 : max (ll[i - 1][j], lo + 1);
				}
			}
			for (int j = n - 1; j >= 0; --j) {
				if (mat[i][j] == 1) {
					rr[i][j] = n; ro = j;
				} else {
					rr[i][j] = i == 0 ? ro - 1 : min (rr[i - 1][j], ro - 1);
					ans = max (ans, up[i][j] * (rr[i][j] - ll[i][j] + 1));
				}
			}
		}
		cout << ans * 3 << endl;
	}
}


例題$23$ 遙遠的銀河(\(LA3695\)

  • 題意:找一個矩形,使其邊界上包括盡可能多的點。
  • 解法:枚舉矩形。
    • $O(N^5)$朴素枚舉矩形每一條邊
    • 考慮固定一些限制條件,比如只枚舉上下邊界,對左右邊的位置想辦法優化維護一下。我們列出來要維護的最大值,用掃描的思想去搞一下就可以了,難度在於細節。
#include <bits/stdc++.h>
using namespace std;

const int N = 100 + 10;

struct Point {
	int x, y;
	bool operator < (Point rhs) const {
		return x < rhs.x;
	}
}P[N];

int n, m, y[N], on[N], on2[N], rem[N];

int solve () {
	sort (P, P + n);
	sort (y, y + n);
	m = unique (y, y + n) - y;
	if (m <= 2) return n;
	
	int ans = 0;
	for (int a = 0; a < m; ++a) {
		for (int b = a + 1; b < m; ++b) {
			int k = 0, ly = y[a], ry = y[b];
			for (int i = 0; i < n; ++i) {
				if (i == 0 || P[i].x != P[i - 1].x) { // 新的豎線 
					++k;
					on[k] = on2[k] = 0;
					rem[k] = rem[k - 1] + on2[k - 1] - on[k - 1];
				}
				if (P[i].y > ly && P[i].y < ry) on[k]++;
				if (P[i].y >= ly && P[i].y <= ry) on2[k]++;
			}
			
			if (k <= 2) return n;
			
			int M = 0;
			for (int j = 1; j <= k; ++j) {
				ans = max (ans, rem[j] + on2[j] + M);
				M = max (M, on[j] - rem[j]);
			} 
		}
	}
	return ans;
}

int main () {
	int kase = 0;
	while (cin >> n && n) {
		for (int i = 0; i < n; ++i) {
			cin >> P[i].x >> P[i].y; y[i] = P[i].y;
		}
		printf ("Case %d: %d\n", ++kase, solve ()); 
	}
}

例題$24$ 廢料堆(\(UVa10755\)

  • 三維立方體,每一塊$(x, y, z)$有一個整數價值,求最大價值子立方體。\(Tips:N <= 20。\)

  • 高維題目考慮降維。

    • 一維時我們有$O(N)$的最大子段和解法。
    • 二維的時候我們可以枚舉某些連續的行,把這些行壓成一個一維序列做最大子段和,復雜度$O(N^3)$
    • 三維的時候我們可以枚舉某些連續的高,把這些搞壓成一個二維矩形做最大子矩陣,復雜度$O(N^5)$
#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 20 + 5;
const int INF = 1e18;

int T, n, m, h, line[N], mat[N][N], w[N][N][N], sumh[N][N][N], sumy[N][N], sumx[N];

int solve_1D () {
	int now = -INF, ans = -INF;
	for (int i = 1; i <= n; ++i) {
		now = max (line[i], now + line[i]);
		ans = max (ans, now);
	}
	return ans;
}

int solve_2D () {
	for (int x = 1; x <= n; ++x) {
		for (int y = 1; y <= m; ++y) {
			sumy[x][y] = sumy[x][y - 1] + mat[x][y];
		}
	}
	int ans = -INF;
	for (int ly = 1; ly <= m; ++ly) {
		for (int ry = ly; ry <= m; ++ry) {
			for (int x = 1; x <= n; ++x) {
				line[x] = sumy[x][ry] - sumy[x][ly - 1];
			}
			ans = max (ans, solve_1D ());
		}
	}
	return ans;
}

int solve_3D () {
	for (int x = 1; x <= n; ++x) {
		for (int y = 1; y <= m; ++y) {
			for (int z = 1; z <= h; ++z) {
				sumh[x][y][z] = sumh[x][y][z - 1] + w[x][y][z];
			}
		}
	}
	int ans = -INF;
	for (int lz = 1; lz <= h; ++lz) {
		for (int rz = lz; rz <= h; ++rz) {
			for (int x = 1; x <= n; ++x) {
				for (int y = 1; y <= m; ++y) {
					mat[x][y] = sumh[x][y][rz] - sumh[x][y][lz - 1];
				}
			} //壓成一個平面 
			ans = max (ans, solve_2D ());
		}
	}
	return ans;
}

signed main () {
//	freopen ("data.in", "r", stdin);
	cin >> T;
	while (T--) {
		cin >> n >> m >> h;
		int x = 1, y = 1, z = 1;
		for (int i = 1; i <= n * m * h; ++i, ++z) {
			if (z > h) y += 1, z -= h;
			if (y > m) x += 1, y -= m;
			cin >> w[x][y][z];
		}
		cout << solve_3D () << endl; if (T) cout << endl;
	} 
} 

例題$25$ 侏羅紀(\(LA2965\)

  • 題意:$N$個字符串,選擇盡可能多的串,使每個字母都可以出現偶數次。\(Tips:N<=24。\)
  • 解法:折半搜索。因為兩部分的可用答案具有可以對應的性質(字母個數對應起來是偶數),合並答案時可以開一個桶(\(Map\))來記錄。復雜度$O(2N)->O(2{N/2}logN)$
#include <bits/stdc++.h>
using namespace std;

const int N = 24 + 5;
const int M = 1000000 + 5;
#define lowbit(x) (x & -x)

int n, val[N]; char s[M];

map <int, int> mp;

int wei (int x) {
	int s = 0;
	while (x) x -= lowbit (x), ++s;
	return s;
}

void dfs1 (int now, int cho, int sit) {
	if (now == n / 2 + 1) {
		// cout << "sit = " << sit << endl;
	    mp[sit] = wei (mp[sit]) > wei (cho) ? mp[sit] : cho;
		// cout << "mp[sit] = " << mp[sit] << endl;
		// cout << "wei[sit] = " << wei (sit) << endl;
	    return;
	}
	dfs1 (now + 1, cho, sit);
	dfs1 (now + 1, cho | (1 << now), sit ^ val[now]);
}

int ans, fin;

void dfs2 (int now, int cho, int sit) {
	if (now == n) {
		// cout << "find : sit = " << sit << endl;
		if (mp.count (sit)) {
			if (ans < wei (cho) + wei (mp[sit])) {
				// cout << "cho = " << cho << " mp[sit] = " << mp[sit] << endl;
				fin = cho | mp[sit];
				ans = wei (cho) + wei (mp[sit]);
			}
		}
		return;
	}
	dfs2 (now + 1, cho, sit);
	dfs2 (now + 1, cho | (1 << now), sit ^ val[now]);
}

int main () {
	// freopen ("data.in", "r", stdin);
	while (cin >> n && n) {
		mp.clear (); ans = fin = 0;
		memset (val, 0, sizeof (val));
		for (int i = 0; i < n; ++i) {
			cin >> s;
			int l = strlen (s);
			for (int j = 0; j < l; ++j) {
				val[i] ^= (1 << (s[j] - 'A'));
			}
			// cout << "val[" << i << "] = " << val[i] << endl;
		}
		dfs1 (0, 0, 0);
		dfs2 (n / 2 + 1, 0, 0);
		cout << ans << endl;
		for (int i = 0; i < n; ++i) {
			if ((fin >> i) & 1) printf ("%d ", i + 1);
		}
		cout << endl;
	}
}

例題$26$ 約瑟夫問題的變形(\(LA3882\)

  • 題意:給你一個環,在上面做約瑟夫,求最后一個被刪除的數\(n, k <=10000\)

  • 因為只關心最后一個被刪除的數,所以可以不考慮中間的直接進行遞推。

  • 設一共有$[0,i-1]$這$i$個時,從$0$開始選擇刪除的最后一個數是$f(i)$。那么有$f(1) = 0,f(x) =(f(x-1)+k)%n。$

  • 考慮方法:刪除一個數后對剩下的再編號。

#include <bits/stdc++.h>
using namespace std;

const int N = 10010;

int n, m, k, f[N];

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n >> k >> m && n) {
		for (int i = 2; i <= n; ++i) f[i] = (f[i - 1] + k) % i;
		int ans = (m - k + 1 + f[n]) % n;
		if (ans <= 0) ans += n;
		cout << ans << endl;
	}
}

例題$27$ 王子和公主(\(UVa10635\)

  • 題意:兩個不含重復元素的序列,求最長公共子序列。
  • 解法:對第二個序列以第一個序列內的元素編號為關鍵字進行排序,轉化為求最長上升子序列。
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int T, n, p, q, A1[N], A2[N], id[N], arr[N];

int main () {
//	freopen ("data.in", "r", stdin);
	cin >> T;
	for (int Case = 1; Case <= T; ++Case) {
		cin >> n >> p >> q; ++p, ++q;
		memset (id, 0, sizeof (id));
		memset (arr, 0, sizeof (arr));
		for (int i = 1; i <= p; ++i) {
			cin >> A1[i];
			id[A1[i]] = i;
		}
		for (int i = 1; i <= q; ++i) {
			cin >> A2[i];
			if (!id[A2[i]]) {
				q = q - 1;
				i = i - 1;
			} else {
				A2[i] = id[A2[i]];
			}
		}
		int tot = 0;
		for (int i = 1; i <= q; ++i) {
			if (A2[i] > arr[tot]) arr[++tot] = A2[i];
			else {
				arr[lower_bound (arr + 1, arr + 1 + tot, A2[i]) - arr] = A2[i];
			}
		}
		cout << "Case " << Case << ": " << tot << endl;
	}
}

例題$28$ \(Sum游戲\)\(UVa10891\)

  • 記憶化搜索的寫法很顯然,枚舉每種后繼狀態做一個$max$,記憶化一下就有$O(N^3)$了。
  • 考慮每個狀態$(i,j)\((剩余\)[i,j]\(這一段)的決策實際上是對狀態矩陣中的\)[i,i->j-1]\(和\)[i+1->j,j]$這兩段取$min$作為舍棄的收益減掉,也就是說我們維護一下這兩個最小值的矩陣,就可以做到$O(N)$轉移了。
//我自己的寫法
#include <bits/stdc++.h>
using namespace std;

const int N = 100 + 5;
const int INF = 0x3f3f3f3f;

int n, sum[N], dp[N][N][2];

int dfs (int l, int r, int p) { // player p -> can choose [l, r];
	int ans = -INF;
	if (l > r) return 0;
	if (dp[l][r][p] != INF) return dp[l][r][p];
	for (int i = l; i <= r; ++i) {
		ans = max (ans, -dfs (i + 1, r, p ^ 1) + sum[i] - sum[l - 1]); // choose [l, i]
		ans = max (ans, -dfs (l, i - 1, p ^ 1) + sum[r] - sum[i - 1]); // choose [i, r] 
	}
	return dp[l][r][p] = ans;
} 

int main () {
	while (cin >> n && n) {
		memset (dp, 0x3f, sizeof (dp));
		for (int i = 1; i <= n; ++i) {
			cin >> sum[i]; sum[i] += sum[i - 1];
		}
		cout << dfs (1, n, 0) << endl;
	}
}
//藍書上的易於優化的記搜寫法
#include <bits/stdc++.h>
using namespace std;

const int N = 100 + 5;
const int INF = 0x3f3f3f3f;

int n, sum[N], dp[N][N];

int dfs (int l, int r) { // player p -> can choose [l, r];
	int ans = 0;
	if (l > r) return 0;
	if (dp[l][r] != INF) return dp[l][r];
	for (int i = l; i < r; ++i) ans = min (ans, dfs (i + 1, r)); // choose [l, i]
	for (int i = r; i > l; --i) ans = min (ans, dfs (l, i - 1));
	return dp[l][r] = sum[r] - sum[l - 1] - ans;
} 

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n && n) {
		memset (dp, 0x3f, sizeof (dp));
		for (int i = 1; i <= n; ++i) {
			cin >> sum[i]; sum[i] += sum[i - 1];
		}
		cout << 2 * dfs (1, n) - sum[n] << endl;
	}
}

//O(N^2)最優解法
#include <bits/stdc++.h>
using namespace std;

const int N = 100 + 5;

int n, A[N], sum[N], f[N][N], g[N][N], dp[N][N];

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n && n) {
		for (int i = 1; i <= n; ++i) {
			cin >> A[i]; 
			sum[i] = sum[i - 1] + A[i];
			f[i][i] = g[i][i] = dp[i][i] = A[i];
		}
		for (int L = 1; L < n; ++L) {
			for (int i = 1; i + L <= n; ++i) {
				int j = i + L, m = 0;
				m = min (m, f[i + 1][j]);
				m = min (m, g[i][j - 1]);
				dp[i][j] = sum[j] - sum[i - 1] - m;
				f[i][j] = min (dp[i][j], f[i + 1][j]);
				g[i][j] = min (dp[i][j], g[i][j - 1]);
			}
		}
		cout << 2 * dp[1][n] - sum[n] << endl;
	}
}

例題$29$ 黑客的攻擊(\(UVa11825\)

  • 數學模型:把$n$個集合$P_1,P_2...P_n$分成盡量多組,使每組中所有集合的並等於全集。

  • 考慮二進制狀態壓縮,然后對分組后的集合進行$DP$。

  • 關鍵思想:用集合的思想去設狀態。復雜度不會證。

  • 這里有我當時寫的更詳細的題解

#include <bits/stdc++.h>
using namespace std;

const int N = 20;

int Case, n, m, to, s[N], f[N], cho[1 << N];

int main () {
//  freopen ("data.in", "r", stdin);
    while (cin >> n && n) {
        for (int i = 0; i < n; ++i) {
            cin >> m; s[i] = 1 << i;
            for (int j = 0; j < m; ++j) {
                cin >> to; s[i] |= 1 << to;
            }   
//          cout << "s[" << i << "] = " << s[i] << endl;
        } 
        const int All = (1 << n) - 1;
        for (int i = 0; i < 1 << n; ++i) {
            cho[i] = 0;
            for (int k = 0; k < n; ++k) {
                if ((i >> k) & 1) {
                    cho[i] |= s[k];
                }
            }
        }
        f[0] = 0;
        for (int S = 1; S < (1 << n); ++S) {
            f[S] = 0;
            for (int S0 = S; S0; S0 = (S0 - 1) & S) { //枚舉S的子集 
                if (cho[S0] == All) {
                    f[S] = max (f[S], f[S ^ S0] + 1);
                }
            }
        }
        cout << "Case " << ++Case << ": " << f[All] << endl;
    }
}

例題$31$ 撿垃圾的機器人(\(LA3983\)

  • 解法:因為要選擇的元素有序,所以可以把原$n$個元素分成連續的$k$段(不用顯式建出這$k$段分段),可以推出來一個$O(能過)$的方程。(吐槽一句:因為$C$實在太小了,不知道出題人想卡什么...)
  • 正解:上面那個轉移方程是可以單調隊列優化的形式,套個單調隊列上去就$O(N)$了。
  • 吐槽:$cin$恐成最大受害者。
//暴力
#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 100010;

int T, n, c, x[N], y[N], w[N], sumd[N], sumw[N], dp[N];

int dis (int i) {return x[i] + y[i];}

signed main () {
	cin >> T;
	while (T--) {
		cin >> c >> n;
		memset (sumw, 0, sizeof (sumw));
		memset (sumd, 0, sizeof (sumd));
		for (int i = 1; i <= n; ++i) {
			cin >> x[i] >> y[i] >> w[i];
			sumw[i] = sumw[i - 1] + w[i];
			sumd[i] = sumd[i - 1] + abs (x[i] - x[i - 1]) + abs (y[i] - y[i - 1]);
		}
		memset (dp, 0x3f, sizeof (dp)); dp[0] = 0;
		for (int i = 1; i <= n; ++i) {
			for (int j = i - 1; j >= 0; --j) {
				if (sumw[i] - sumw[j] > c) break;
//				printf ("dp[%I64d] <- dp[%I64d] = %I64d\n", i, j, dp[j]);
//				printf ("       <- dis(%I64d) = %I64d\n", i, dis (i));
//				printf ("       <- dis(%I64d) = %I64d\n", j + 1, dis (j + 1));
//				printf ("       <- sumd (%I64d -> %I64d) = %I64d\n", j + 1, i, sumd[i] - sumd[j + 1]);
				dp[i] = min (dp[i], dp[j] + dis (i) + dis (j + 1) + sumd[i] - sumd[j + 1]);
			}
//			cout << "dp[" << i << "] = " << dp[i] << endl;
		}
		cout << dp[n] << endl; if (T) cout << endl;
	} 
}
//正解
#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N = 100010;

int T, n, c, x[N], y[N], w[N], sumd[N], sumw[N], dp[N];

int dis (int i) {return x[i] + y[i];}

struct Node {
	int pos, val;
}q[N];

int head, tail;

void Insert (int pos) {
	int val = dp[pos] + dis (pos + 1) - sumd[pos + 1];
	while (head <= tail && q[tail].val >= val) --tail;
	q[++tail] = (Node) {pos, val};
}

int front (int pos) {
	while (head <= tail && sumw[pos] - sumw[q[head].pos] > c) ++head;
	return q[head].val;
}

signed main () {
//	freopen ("data.in", "r", stdin);
	cin >> T;
	while (T--) {
		cin >> c >> n;
		memset (sumw, 0, sizeof (sumw));
		memset (sumd, 0, sizeof (sumd));
		for (int i = 1; i <= n; ++i) {
			cin >> x[i] >> y[i] >> w[i];
			sumw[i] = sumw[i - 1] + w[i];
			sumd[i] = sumd[i - 1] + abs (x[i] - x[i - 1]) + abs (y[i] - y[i - 1]);
		}
		head = 1, tail = 0;
		dp[0] = 0; Insert (0);
		for (int i = 1; i <= n; ++i) {
//			for (int j = i - 1; j >= 0; --j) {
//				if (sumw[i] - sumw[j] > c) break;	
//				printf ("dp[%I64d] <- dp[%I64d] = %I64d\n", i, j, dp[j]);
//				printf ("       <- dis(%I64d) = %I64d\n", i, dis (i));
//				printf ("       <- dis(%I64d) = %I64d\n", j + 1, dis (j + 1));
//				printf ("       <- sumd (%I64d -> %I64d) = %I64d\n", j + 1, i, sumd[i] - sumd[j + 1]);
//				dp[i] = min (dp[i], dp[j] + dis (i) + dis (j + 1) + sumd[i] - sumd[j + 1]);
//			}
//			cout << "dp[" << i << "] = " << dp[i] << endl;
			dp[i] = dis (i) + sumd[i] + front (i); Insert (i);
		}
		cout << dp[n] << endl; if (T) cout << endl;
	} 
}


例題$32$ 分享巧克力(\(LA4794\)

  • 題意:$N*M$的矩形,每次可以用一條直線把它划分成兩部分,問能不能切成面積為$a_1,a_2...a_n$的$n$個矩形部分。

  • 考慮記搜,設狀態$f(r,c,S)$,表示$r*c$的矩形是否能恰好划分為$S$集合中的所有矩形,然后就是一個枚舉子集的記搜題目了。

#include <bits/stdc++.h>
using namespace std;

const int M = 15 + 1;
const int N = 100 + 5;

int n, x, y, kase, p[N], sum[1 << M];
bool dp[N][1 << M], vis[N][1 << M];

int bitcount (int x) {
	return x == 0 ? 0 : bitcount (x >> 1) + (x & 1); 
}

bool dfs (int x, int S) {
	if (vis[x][S]) return dp[x][S];
	if (bitcount (S) == 1) return true;
	int y = sum[S] / x; vis[x][S] = true;
	for (int S0 = S & (S - 1); S0; S0 = S & (S0 - 1)) {
		int S1 = S ^ S0; // 拆分成兩個集合 S 和 S0 
		if (sum[S0] % x == 0) {
			if (dfs (min (x, sum[S0] / x), S0) && dfs (min (x, sum[S1] / x), S1)) {
				return dp[x][S] = true;
			}
		}
		if (sum[S0] % y == 0) {
			if (dfs (min (y, sum[S0] / y), S0) && dfs (min (y, sum[S1] / y), S1)) {
				return dp[x][S] = true;
			}
		} 
	}
	return dp[x][S] = false;
}

int main () {
//	freopen ("data.in", "r", stdin);
	while (cin >> n && n) {
		cin >> x >> y;
		memset (dp, 0, sizeof (dp));
		memset (vis, 0, sizeof (vis));
		memset (sum, 0, sizeof (sum));
		for (int i = 0; i < n; ++i) cin >> p[i];
		for (int i = 0; i < (1 << n); ++i) {
			for (int k = 0; k < n; ++k) {
				if (i & (1 << k)) sum[i] += p[k];
			}
		} 
		int All = (1 << n) - 1; 
		if (sum[All] != x * y || sum[All] % x != 0) {
			cout << "Case " << ++kase << ": " << "No" << endl;
		} else {
			cout << "Case " << ++kase << ": " << (dfs (min (x, y), All) ? "Yes" : "No") << endl;
		}
	}
}

\(QQ:757973845\)。博主學習時比較倉促,博客中不清晰處,錯誤之處,還請指正:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM