POJ 3692: Kindergarten

http://poj.org/problem?id=3692

男女がお互いを知っている最大の集合を求める。
これは男女がお互いを知らない、ということはない最大の集合であるので、
知り合いではない男女を辺でつないで最大安定集合を求めれば良い。
あとは|最大安定集合|=V-|最大マッチング|でできる。

最大安定集合はなんていうか否定が絡むイメージ。

bool V[MAX_N][MAX_N];
int N, M, G;

int main() {
	for(int q = 1; ; q++) {
		scanf("%d%d%d", &N, &M, &G);
		if(N == 0 && M == 0 && G == 0) break;
		memset(V, 0, sizeof(V));
		MF::init(N + M + 2);
		rep(i, 0, G) {
			int a, b; scanf("%d%d", &a, &b);
			a--; b--;
			V[a][b + N] = true;
		}
		int s = N + M, t = N + M + 1;
		rep(i, 0, N) {
			rep(j, N, M + N) {
				if(!V[i][j]) MF::add_edge(i, j, 1);
			}
		}
		rep(i, 0, N) MF::add_edge(s, i, 1);
		rep(i, 0, M) MF::add_edge(i + N, t, 1);

		printf("Case %d: %lld\n", q, N + M - MF::get(s, t));
	}


	return 0;
}

Kindergardenのミススペルだと思ったら、Kindergartenの方が正しかった…

POJ 1466: Girls and Boys

http://poj.org/problem?id=1466

最大安定集合を求める。
二部グラフになるので|最小点カバー|=|最大マッチング|、
任意のグラフについて|最小点カバー|+|最大安定集合|=|V|よりできる。
頂点を2倍にしてあげると実装が楽。



簡単な証明いろいろ。

  • |最大マッチング|+|最小辺カバー|=|V|

辺を追加すると新たにカバーできる点は2つor1つor0つ。0つは明らかに無駄であり、なるべく2つずつカバーしたい。
ここで2つずつカバーできる最大回数=|最大マッチング|なので残りの点はそれらと同じ数だけの辺でカバーする。
これで最小辺カバーが得られる。よって
|最小辺カバー| = (新たに1つ点をカバーする辺)+(2つ点をカバーする辺)=(V-2*M)+M=V-M (M=|最大マッチング|)となり示せた。

  • |最小点カバー|+|最大安定集合|=|V|

XがGの安定集合<=>V-XがGの点カバー を示す。
(=>)V-Xはある辺を通じてXと必ずつながっている。なぜならXの点どおしは互いに隣接しておらず、V-Xの点によって囲われているから。
(<=)もしXに隣接している2点が存在したとすると、その間の辺はカバーできておらず、V-Xが点カバーであることに矛盾。
よって|点カバー|+|安定集合|=|V|であり、点カバーが最小なら安定集合は最大となるので示せた。

  • 二部グラフ(U,V)において|最大マッチング|=|最小点カバー|

最大マッチングにより|S|-|Γ(S)|を最大化、|T|-|Γ(T)|を最小化する点の集合S,Tを求められることを示す。
結論から言うと、最小カットc(A,B)にはU,V間の辺を通らないものが存在する(アルゴリズムデザイン335ページ参照)ので、その時S=A∩U,T=B∩Vとすればよい。
このとき|最小カット|=|A∩V|+|B∩U|=Γ(S)+|U|-|S|=|U|-(|S|-|Γ(S)|)となり、|U|が定数であることより示せた。
また|S|=|U|-|T|,|Γ(S)|=|V|-|Γ(T)|より|T|-|Γ(T)|が最小化されることも示せた。
あとはΓ(S)とTを点カバーの集合に使えば、最小の点でカバーできることがわかる。
ところで|Γ(S)|+|T|=|A∩V|+|B∩U|=|最小カット|=|最大マッチング|なので示せた。

int N;

int main() {
	while(scanf("%d", &N) != EOF) {
		int s = 2 * N, t = 2 * N + 1;
		MF::init(2 * N + 2);
		rep(i, 0, N) {
			int a, b;
			scanf("%d: (%d)", &a, &b);
			rep(j, 0, b) {
				int c; scanf("%d", &c);
				MF::add_edge(a, c + N, 1);
			}
			MF::add_edge(s, i, 1);
			MF::add_edge(i + N, t, 1);
		}
		printf("%lld\n", N - MF::get(s, t) / 2);
	}
	return 0;
}

POJ 1486: Sorting Slides

http://poj.org/problem?id=1486

完全マッチングに必ず使われる辺を答えればよい。
それはフローが流れた辺を削除したとき完全マッチングになるかどうかで判断すればよい。

その際に押し戻すテクを使ってN-1のフローを作り、対象の辺の容量を0にしてdfsすると高速。

フローのライブラリを少しいじるだけでできる。

フローに限らず再帰の引数でバグらせがちなので注意。特にデフォルトの値を設定するとき。

int S[MAX_N], T[MAX_N];

namespace MF { //init before you use it

	struct edge {int to, cap, rev; };

	vector<edge> G[MAX_N];
	bool used[MAX_N];

	void init(int n) {
		rep(i, 0, n) G[i].clear();
		memset(used, 0, sizeof(used));
	}

	int add_edge(int from, int to, int cap) {
		G[from].push_back((edge){to, cap, (int)G[to].size()});
		G[to].push_back((edge){from, 0, (int)G[from].size() - 1});
		return (int)G[from].size() - 1;
	}

	int dfs(int v, int t, int f, bool change = true) {
		if(v == t) return f;
		used[v] = true;
		for(int i = 0; i < (int)G[v].size(); i++) {
			edge &e = G[v][i];
			if(!used[e.to] && e.cap > 0) {
				int d = dfs(e.to, t, min(f, e.cap), change);
				if(d > 0) {
					if(change) {
						e.cap -= d;
						G[e.to][e.rev].cap += d;
					}
					return d;
				}
			}
		}
		return 0;
	}

	ll get(int s, int t) {
		ll flow = 0;
		while(true) {
			memset(used, 0, sizeof(used));
			int f = dfs(s, t, inf);
			if(f == 0) return flow;
			flow += f;
		}
	}

	bool ok(int v, int k, int n) {
		int s = 2 * n, t = 2 * n + 1;
		int u = G[v][k].to, rev = G[v][k].rev;
		G[u][rev].cap = 0;
		swap(G[s][S[v]].cap, G[G[s][S[v]].to][G[s][S[v]].rev].cap);
		swap(G[u][T[u]].cap, G[G[u][T[u]].to][G[u][T[u]].rev].cap);
		bool res = true;

		memset(used, 0, sizeof(used));
		if(dfs(s, t, inf, false)) res = false;

		G[u][rev].cap = 1;
		swap(G[s][S[v]].cap, G[G[s][S[v]].to][G[s][S[v]].rev].cap);
		swap(G[u][T[u]].cap, G[G[u][T[u]].to][G[u][T[u]].rev].cap);
		return res;
	}
}

int N;
int X1[MAX_N], Y1[MAX_N], X2[MAX_N], Y2[MAX_N];
int X[MAX_N], Y[MAX_N];

vector<pi> ans;

int main() {
	for(int q = 1; ; q++) {
		scanf("%d", &N);
		if(N == 0) break;
		rep(i, 0, N) scanf("%d%d%d%d", &X1[i], &X2[i], &Y1[i], &Y2[i]);
		rep(i, 0, N) scanf("%d%d", &X[i], &Y[i]);

		int s = 2 * N, t = 2 * N + 1;
		MF::init(2 * N + 2);
		ans.clear();

		rep(i, 0, N) {
			rep(j, 0, N) {
				if(X1[i] <= X[j] && X[j] <= X2[i] && Y1[i] <= Y[j] && Y[j] <= Y2[i]) {
					MF::add_edge(i, j + N, 1);
				}
			}
		}
		rep(i, 0, N) {
			S[i] = MF::add_edge(s, i, 1);
			T[i + N] = MF::add_edge(i + N, t, 1);
		}
		bool ok = true;
		if(MF::get(s, t) != N) ok = false;
		
		if(ok) {
			rep(i, 0, N) {
				rep(j, 0, (int)MF::G[i].size()) {
					MF::edge e = MF::G[i][j];
					//debug(i, e.to, e.cap);
					if(e.cap == 0) {
						if(MF::ok(i, j, N)) ans.push_back(pi(i, e.to));
					}
				}
			}
		}
		printf("Heap %d\n", q);
		if(ok && (int)ans.size() > 0) {
			rep(i, 0, (int)ans.size()) {
				printf("(%c,%d)%c", ans[i].fst + 'A', ans[i].sec - N + 1, i == (int)ans.size() - 1 ? '\n' : ' ');
			}
		}
		else printf("none\n");
		printf("\n");
	}
	return 0;
}

outputの形式がややこしい。問題文は読みましょう。

POJ 2112: Optimal Milking

にぶたんして最大流流す。

にぶたんの初期値を小さくしすぎてWA出した…

struct edge {int to, cap, rev; };

vector<edge> G[MAX_N];
bool used[MAX_N];

void add_edge(int from, int to, int cap) {
	G[from].push_back((edge){to, cap, (int)G[to].size()});
	G[to].push_back((edge){from, 0, (int)G[from].size() - 1});
}

int dfs(int v, int t, int f) {
	if(v == t) return f;
	used[v] = true;
	for(int i = 0; i < (int)G[v].size(); i++) {
		edge &e = G[v][i];
		if(!used[e.to] && e.cap > 0) {
			int d = dfs(e.to, t, min(f, e.cap));
			if(d > 0) {
				e.cap -= d;
				G[e.to][e.rev].cap += d;
				return d;
			}
		}
	}
	return 0;
}

ll max_flow(int s, int t) {
	ll flow = 0;
	while(true) {
		memset(used, 0, sizeof(used));
		int f = dfs(s, t, inf);
		if(f == 0) return flow;
		flow += f;
	}
}


int K, C, M; //machine, cow
int D[250][250];

bool ok(int m) {
	memset(used, 0, sizeof(used));
	rep(i, 0, K + C + 2) {
		G[i].clear();
	}
	int s = K + C, t = K + C + 1;
	rep(i, K, K + C) {
		rep(j, 0, K) {
			if(D[i][j] <= m) {
				//debug(m, i, j, D[i][j]);
				add_edge(i, j, 1);
			}
		}
	}
	rep(i, K, K + C) add_edge(s, i, 1);
	rep(i, 0, K) add_edge(i, t, M);
	return max_flow(s, t) == C;
}

int main() {
	scanf("%d%d%d", &K, &C, &M);
	rep(i, 0, K + C) {
		rep(j, 0, K + C) {
			scanf("%d", &D[i][j]);
			if(i == j) D[i][j] = 0;
			else if(D[i][j] == 0) D[i][j] = inf;
		}
	}

	rep(k, 0, K + C) {
		rep(i, 0, K + C) {
			rep(j, 0, K + C) {
				MIN(D[i][j], D[i][k] + D[k][j]);
			}
		}
	}

	/*
	rep(i, 0, K + C) {
		debug(vi(D[i], D[i] + K + C));
	}
	*/

	int lv = 0, rv = 100000;
	while(rv - lv > 1) {
		int m = (lv + rv) / 2;
		if(ok(m)) rv = m;
		else lv = m;
	}
	printf("%d\n", rv);

	return 0;
}

難しくはないけどWA出すとフローパートが間違ってたと思っちゃうよね。
フローはクラスじゃなくてnamespaceで管理するのがいいね。

POJ 2914: Minimum Cut

http://poj.org/problem?id=2914

これもフローとはまた違った話。

全域最小カット。Stoer–Wagnerを使う。

Stoer–Wagnerは
「点の集合からの辺のコストの合計が大きいもの点から取っていって、最後から二番目に取った点sと最後に取った点tの最小カットは
tから生えてる辺のコストの合計になる」
という定理に基づいている。

この証明はw(Av,v)<=w(Cv)をvの一つ前に取った点uについての仮定w(Au,u)<=w(Cu)から示し、帰納法を使う。
(ただしAvはvよりも前に取った点集合、Cvはs-tカットのうち(Av or v)の集合に含まれてる辺の合計、w(Av,v)はAvからvへの辺のコストの合計)
補集合を考えると仮定をうまく利用できる。

詳しくは↓を参照。
https://www.slideshare.net/hcpc_hokudai/flow-cut

int global_min_cut(vector<vector<int> > g) { //g: n*n matrix of graph. if no edge, the cost is zero
	int n = (int)g.size(), res = inf; 
	vector<int> redV(n); 
	rep(i, 0, n) redV[i] = i; 
	rer(rem, n + 1, 2) { //calc MA order 
		int u = 0, v = 0, cut = 0; 
		vector<int> w(rem,0); 
		rep(i, 0, rem){ 
			u = v; v = max_element(all(w)) - w.begin(); 
			cut = w[v]; w[v] = -1; 
			rep(p, 0, rem) {
				if(w[p] >= 0) w[p] += g[redV[v]][redV[p]]; 
			}
		}
		//merge graph 
		rep(i,0, rem) { 
			g[redV[u]][redV[i]] += g[redV[v]][redV[i]]; 
			g[redV[i]][redV[u]] += g[redV[i]][redV[v]]; 
		} 
		redV.erase(redV.begin() + v); 
		//update min_cut 
		res = min(res, cut);
	} 
	return res; 
}

int N, M;

int main() {
	while(scanf("%d%d", &N, &M) != EOF) {
		vector<vector<int> > g(N, vector<int>(N, 0));
		rep(i, 0, M) {
			int a, b, c; scanf("%d%d%d", &a, &b, &c);
			g[a][b] += c;
			g[b][a] += c;
		}
		printf("%d\n", global_min_cut(g));
	}
	return 0;
}

補集合を考えるのはフローに帰着する際の頻出テク臭いね。

POJ 3713: Transferring Sylla

Mengerの定理より全域最小点カットを求めれば良い。

すべての点対に対してフローを流せばできるが、それではTLE。
全域最小辺カットならO(V^3)のアルゴリズム(Stoer-Wagner)が存在するが、全域最小点カットは見つけられなかった。

全域最小点カットが2以上になるかは関節点が存在しないことに言い換えられるので、
3以上になるかはどの1点を消しても関節点が存在しないことと同値。

3という数字が絶妙。4以上だったらフローの方が速い。

int N, M;
vector<int> G[MAX_N];
bool used[MAX_N];
int low[MAX_N], ord[MAX_N];


void lowlink(int at, int p, int &k, int ng) {
//int &k can be altered as int k
//used this as lowlink(0, -1, k). declare int k = 0
//if(ord[at] < low[n]) (n, at) is a bridge
//if(at == 0) if(nG[at].size() > 1) at  is an articulation point
//else if(ord[at] <= low[n]) at  is an articulation point
	used[at] = true;
	low[at] = ord[at] = k;
	k++;
	for(int i = 0; i < G[at].size(); i++) {
		int n = G[at][i];
		if(!used[n] && n != ng) {
			lowlink(n, at, k, ng);
			low[at] = min(low[at], low[n]);
		}
		else if(n != p && n != ng) low[at] = min(low[at], ord[n]);
	}
}


int dfs(int at, int root, int ng) {
	used[at] = true;
	bool tmp = false;
	int cnt = 0, res = 0;
	for(int i = 0; i < G[at].size(); i++) {
		int n = G[at][i];
		if(used[n] || n == ng) continue;
		if(at != root && ord[at] <= low[n] && !tmp) {
			res++;
			tmp = true;
		}
		res += dfs(n, root, ng);
		cnt++;
	}
	if(at == root && cnt > 1) {
		res++;
	}
	return res;
}

int main() {
	while(true) {
		scanf("%d%d", &N, &M);
		if(N == 0 && M == 0) break;
		rep(i, 0, N) G[i].clear();

		rep(i, 0, M) {
			int a, b; scanf("%d%d", &a, &b);
			G[a].push_back(b);
			G[b].push_back(a);
		}
		bool found = false;
		rep(i, 0, N) {
			
			memset(low, 0, sizeof(low));
			memset(used, 0, sizeof(used));
			memset(ord, 0, sizeof(ord));

			int k = 0;
			if(i != 0) {
				lowlink(0, -1, k, i);
				memset(used, 0, sizeof(used));
				int a = dfs(0, 0, i);
				if(a) {
					found = true;
					break;
				}
			}
			else {
				lowlink(1, -1, k, i);
				memset(used, 0, sizeof(used));
				int a = dfs(1, 1, i);
				if(a) {
					found = true;
					break;
				}
			}
			rep(j, 0, N) {
				if(j != i && !used[j]) {
					found = true;
					break;
				}
			}
			if(found) break;
		}
		if(found) printf("NO\n");
		else printf("YES\n");
	}
	return 0;
}

Mengerの定理を使っているからフローの考えを使うといえなくもないけど…

POJ 2987: Firing

http://poj.org/problem?id=2987

はじめてフローでまともな問題解いたかもしれない。

maximum closure problemという有名問題。↓wiki参照。
https://en.wikipedia.org/wiki/Closure_problem

何個使うかも答えなければいけないが、それはmin-cutを得る際の(cap>0)の辺のみを追っていくdfsでできる。

bool used[MAX_N];

void group(int v) {
	used[v] = true;
	rep(i, 0, (int)G[v].size()) {
		int n = G[v][i].to;
		if(!used[n] && G[v][i].cap > 0) {
			group(n);
		}
	}
}

int N, M;

int main() {
	scanf("%d%d", &N, &M);
	int s = 0, t = N + 1;
	ll sum = 0;
	rep(i, 0, N) {
		int a; scanf("%d", &a);
		if(a > 0) {
			add_edge(s, i + 1, a);
			sum += a;
		}
		else add_edge(i + 1, t, -a);
	}
	rep(i, 0, M) {
		int a, b; scanf("%d%d", &a, &b);
		add_edge(a, b, inf);
	}

	ll res = sum - max_flow(s, t);
	group(0);

	int g = 0;
	rep(i, 0, N + 2) g += used[i];
	if(res < 0) printf("0, 0\n");
	else printf("%d %lld\n", g - 1, res);
	return 0;
}

なんかフローって不思議な感じがする…。
この問題も直観的には理解できてない…。