AGC017D: Game on Tree

http://agc017.contest.atcoder.jp/tasks/agc017_d

(ある点vのsubtree)と(vから親pへ向かう辺)で構成されるグラフについてgrundyが求まれば、あとはXORをとれば、pのsubtreeに対してgrundy(p)が求まります。grundy(v)は再帰的に求まるとして、このグラフにvからpへ向かう辺が追加されたとき、grundy数は何になるかが問題になります。

おそらくgrundy(v)+1になることが予想されるので、grundy数についての帰納法で解けばええやん!と思いましたがこれは無理です。
grundy数に対して帰納法は適応できないということです。
grundy(v)=0..k-1まで示せているとしてgrundy(v)=kを考えてみましょう。するとsubtree+1辺のグラフから1辺取り除いた時、grundy数がkよりも大きい値をとる可能性があります。kよりも大きいgrundy数は仮定にないので失敗となります。

なので頂点に対して帰納法をしましょう。すると簡単に示せます。

木は構成要素が多くて、注目するものを間違うとスムーズに解けないことを再確認しました。

int N;
vector<int> G[MAX_N];

int dfs(int v, int p = -1) {
	int res = 0;
	rep(i, 0, G[v].size()) {
		int n = G[v][i];
		if(n == p) continue;
		res ^= (dfs(n, v) + 1);
	}
	return res;
}

void solve() {
	cin >> N;
	rep(i, 0, N - 1) {
		int a, b;
		cin >> a >> b; a--; b--;
		G[a].pb(b);
		G[b].pb(a);
	}
	if(dfs(0)) cout << "Alice\n";
	else cout << "Bob\n";
}

grundy数

grundy数について勉強したのでお話します。

grundy数はご存知の通り、ゲームの勝敗を求める際に使う数なのですが、こんな感じの性質があると思います。

1.grundy数は非負整数であり、grundy数=0ならば負け、grundy数>=1ならば勝ちを意味する。
2.grundy数=kの状態からは、0...k-1の状態へ行ける。
3.grundy数を増やすmoveは意味がない
4.N個のgrundy数はXORを適応して一つのgrundy数にできる

1.2.まあこれは定義なんですが、ゲームの勝敗の決め方をきちんと押さえています。
ゲームにおける勝敗は次のように言い換えられます。
「勝ち」<=>次に遷移できる状態で「負け」が存在する。
「負け」<=>次に遷移できる状態がすべて「勝ち」である。

勝ちの条件は「存在する」なのに対し、負けの条件は「すべて」なので負けの方が条件が厳しいのがわかると思います。
なのでゲームの問題を解く際は「負け」の状態は何なのかを一番最初に考えるのがよさそうですね。

grundy数の話に戻すと、負けというのは次にどう行動しても「勝ち」の状態になってしまう、
いわばどんづまりの状況なのでgrundy数=0とする感じです。


3.grundy数を増やす行動をしたとしましょう、すると相手はgrundy数をもとに戻すような行動をとることが出来ますし、さらに元のgrundy数よりも小さいgrundy数の状態に遷移するような行動をした場合、自分はターンを無駄にしたということになります。

grundy数を求める際、遷移できない状態の最小のgrundy数を考え、それよりも大きい遷移可能な状態のgrundy数は無視しますが、これは3が成立するからです。

4.Nimの考え方と同じです。
Nimでは
「負け」=N個の値をXORした値が0になる
ですが、これは上のゲームの勝敗の言い換えを適応すると簡単に示せます。
複数の値を1つの値で扱えるのはXORの結合法則(演算の順番が関係ない性質)のおかげですね。
grundyも1.2.3の性質から4を示すことが出来ます。


ゲームの問題は苦手意識があったけど、grundy数を学ぶことによって、少し考え方がわかった気がする。

POJ 3690: Constellations

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

蟻本に書いてあるように二次元hashをすればいいです。

hashといえばこの記事
http://hos.ac/blog/#blog0003
が有名ですが、ここに書いてあるように3つのmodでhashをとるようにしてみました。

注意:TLE解法です。

struct ll3 {
	ll v[3];
	ll3(ll a,ll b,ll c){v[0]=a;v[1]=b;v[2]=c;}
	ll3(ll a){v[0]=a;v[1]=a;v[2]=a;}
	ll3(){v[0]=0;v[1]=0;v[2]=0;}
	inline void operator=(const ll3& vs){rep(i,0,3)v[i]=vs.v[i];}
	inline void operator=(ll a){rep(i,0,3)v[i]=a;}
	inline ll3 operator+(const ll3& vs){ll3 res;rep(i,0,3)res.v[i]=v[i]+vs.v[i];return res;}
	inline ll3 operator-(const ll3& vs){ll3 res;rep(i,0,3)res.v[i]=v[i]-vs.v[i];return res;}
	inline ll3 operator*(const ll3& vs){ll3 res;rep(i,0,3)res.v[i]=v[i]*vs.v[i];return res;}
	inline ll3 operator%(const ll3& vs){ll3 res;rep(i,0,3)res.v[i]=v[i]%vs.v[i];return res;}
	inline void operator+=(const ll3& vs){rep(i,0,3)v[i]+=vs.v[i];}
	inline void operator-=(const ll3& vs){rep(i,0,3)v[i]-=vs.v[i];}
	inline void operator*=(const ll3& vs){rep(i,0,3)v[i]*=vs.v[i];}
	inline void operator%=(const ll3& vs){rep(i,0,3)v[i]%=vs.v[i];}
	inline ll3 operator+(ll a){ll3 res;rep(i,0,3)res.v[i]=v[i]+a;return res;}
	inline ll3 operator-(ll a){ll3 res;rep(i,0,3)res.v[i]=v[i]-a;return res;}
	inline ll3 operator*(ll a){ll3 res;rep(i,0,3)res.v[i]=v[i]*a;return res;}
	inline ll3 operator%(ll a){ll3 res;rep(i,0,3)res.v[i]=v[i]%a;return res;}
	inline void operator+=(ll a){rep(i,0,3)v[i]+=a;}
	inline void operator-=(ll a){rep(i,0,3)v[i]-=a;}
	inline void operator*=(ll a){rep(i,0,3)v[i]*=a;}
	inline void operator%=(ll a){rep(i,0,3)v[i]%=a;}
	inline bool operator==(const ll3& vs){int cnt=0;rep(i,0,3)cnt+=v[i]==vs.v[i];return cnt==3;}
	inline bool operator!=(const ll3& vs){int cnt=0;rep(i,0,3)cnt+=v[i]==vs.v[i];return cnt<=2;}
	friend ostream& operator<<(ostream& out,const ll3& vs){out<<"("<<vs.v[0]<<" "<<vs.v[1]<<" "<<vs.v[2]<<")";return out;}
};
bool operator<=(const ll3& vs1,const ll3& vs2){rep(i,0,3)if(vs1.v[i]!=vs2.v[i])return vs1.v[i]<=vs2.v[i];return true;}
bool operator<(const ll3& vs1,const ll3& vs2){rep(i,0,3)if(vs1.v[i]!=vs2.v[i])return vs1.v[i]<vs2.v[i];return false;}
const ll3 mod = ll3{1000000007, 1000000009, 1000000021};

int N, M, T, P, Q;
char s[1010];
int patterns[1110][1110];
int field[1110][1110];
ll3 has[1110][1110], tmp[1110][1110];

void compute_hash(int field[1110][1110], int N, int M) {
	rep(i, 0, N + 1) {
		fill(has[i], has[i] + M + 1, 0);
		fill(tmp[i], tmp[i] + M + 1, 0);
	}
	rep(i, 0, N) {
		ll3 h = 0, mul = 1;
		rep(j, 0, Q) {
			h = h * 2 + field[i][j]; h %= mod;
			mul *= 2; mul %= mod;
			//debug(i, j, field[i][j], h, mul);
		}
		for(int j = 0; j + Q <= M; j++) {
			tmp[i][j] = h;
			h = (h * 2 % mod - mul * field[i][j] % mod + field[i][j + Q] + mod) % mod;
		}
	}
	ll3 bmul = 1;
	rep(i, 0, Q) {
		bmul *= 2;
		bmul %= mod;
	}
	for(int j = 0; j + Q <= M; j++) {
		ll3 h = 0, mul = 1;
		rep(i, 0, P) {
			h = h * bmul % mod + tmp[i][j];
			mul *= bmul; mul %= mod;
		}
		for(int i = 0; i + P <= N; i++) {
			has[i][j] = h;
			h = (h * bmul % mod - mul * tmp[i][j] % mod + tmp[i + P][j] + mod) % mod;
		}
	}
}

int main() {
	for(int q = 1; ;q++) {
	scanf("%d%d%d%d%d", &N, &M, &T, &P, &Q);
	if(N == 0) break;
	rep(i, 0, N) {
		scanf("%s", s);
		rep(j, 0, M) {
			if(s[j] == '*') field[i][j] = 1;
			else field[i][j] = 0;
		}
	}
	multiset<ll3> unseen;
	rep(t, 0, T) {
		rep(i, 0, P) {
			scanf("%s", s);
			rep(j, 0, Q) {
				if(s[j] == '*') patterns[i][j] = 1;
				else patterns[i][j] = 0;
			}
		}
		compute_hash(patterns, P, Q);
		unseen.insert(has[0][0]);
	}

	compute_hash(field, N, M);
	for(int i = 0; i + P <= N; i++) {
		for(int j = 0; j + Q <= M; j++) {
			unseen.erase(has[i][j]);
		}
	}
	int ans = T - unseen.size();
	printf("Case %d: %d\n", q, ans);
	}
	return 0;
}

このll3っていうクラスは3つのlong longの値を保持するだけのクラスです。modは自動で取ってくれないのでオーバーフローには注意しないといけません。3つの値に対してそれぞれ異なるmodを取ることによってハッシュの衝突をなるべく避けようとしています。
2つのハッシュ値が等しいかどうかは、3つの値がすべて等しいかで判断します。
つまり2つのハッシュ値が異なると判断された場合は3つの値のうち1つ以上異なることになります。
ハッシュの衝突は異なるものを等しいと判断してしまう現象なので、異なるものをちゃんと異なるといえることが重要になります。
1つのmodに対して本来異なるのに等しいと判断してしまう確率はたいていの場合大体1/modで、さらにそれが3回起こる確率は
modが10^-9であれば10^-27乗のオーダーになるので、衝突することはほとんどないとみていいことがわかります。

ですが、3つのlong longの値に対してmodをとるので遅いしメモリも食います。制限時間の厳しい問題では使用すべきではないでしょう。実際上の解法もTLEしましたし。だからってhashを1つのmodでやるのは怖いしハッシュは加減が難しいですね…

まあ悪意の満ちたテストケース(基底を何個持ってきても衝突する)がない限り大丈夫なのでPOJではmod2^64を使います…

SA+LCP+RMQ

SA+LCP+RMQで任意の2つのsuffixの組について先頭何文字が一致しているかをlog(N)で求めることができる。
前処理も全部O(N)でやることもでき、高速。(下のコードはRMQの前処理がO(NlogN)だが)

HL分解した木に乗せることを考えながらライブラリ化した。

struct T {
	ll v;
	T(ll v = inf) : v(v) {}
	T operator+(const T& t) { return T(min(v, t.v)); }
	friend ostream& operator<<(ostream& out, const T& t) {
		out << t.v; return out;
	}
};

struct segtree{
	int n; vector<T> seg;
	void init(int mx) {
		n = 1;
		while(n < mx) n *= 2;
		seg = vector<T>(n * 2);
	}
	segtree(int mx = 0) {
		init(mx);
	}
	void update(int i, T x) {
		i += n - 1;
		seg[i] = x;
		while(i > 0) {
			i = (i - 1) / 2;
			seg[i] = seg[i * 2 + 1] + seg[i * 2 + 2];
		}
	}
	T ga(int a, int b, int k, int l, int r) {
		if(b <= l || r <= a) return T();
		if(a <= l && r <= b) return seg[k];
		else {
			T lv = ga(a, b, k * 2 + 1, l, (l + r) / 2);
			T rv = ga(a, b, k * 2 + 2, (l + r) / 2, r);
			return lv + rv;
		}
	}
	void show() {
		vector<T> tmp;
		rep(i, 0, n) tmp.push_back(get(i, i + 1));
		debug(tmp);
	}
	//edit from here
	T get(int a, int b) { return ga(a, b, 0, 0, n); } //[a, b)
};

struct SA { //get_sa: don't forget to specify K. default:26
	int n;
	vi S, sa, rank, bkt; //i in S is rank[i] in sa
	vi lcp; segtree seg;
	void init(const vi& sin, int K = 26) {
		n = (int)sin.size();
		S.resize(n + 1); rank.resize(n + 1);
		rep(i, 0, n) S[i] = sin[i] + 1;
		bkt.resize(max(n + 1, K + 1));
		sa = get_sa(S, K + 1);
		rep(i, 0, n + 1) rank[sa[i]] = i;
		get_lcp(); //if you don't want lcp, comment out
		seg.init(n); //if you don't want seg, comment out
		rep(i, 0, n) seg.update(i, lcp[i]);
	}
	inline bool isLMS(const vi& t, int i) {
		if(i > 0 && t[i] && !t[i - 1]) return true;
		else return false;
	}
	void getBuckets(const vi& s, int K, bool end) {
		int sum = 0, n = (int)s.size();
		fill(bkt.begin(), bkt.begin() + K, 0);
		rep(i, 0, n) bkt[s[i]]++;
		rep(i, 0, K) {
			sum += bkt[i];
			bkt[i] = end ? sum : sum - bkt[i];
		}
	}
	void induceSAl(const vi& t, const vi& s, vi& SA, int K) {
		int n = (int)SA.size();
		getBuckets(s, K, false);
		rep(i, 0, n) {
			int j = SA[i] - 1;
			if(j >= 0 && !t[j]) SA[bkt[s[j]]++] = j;
		}
	}
	void induceSAs(const vi& t, const vi& s, vi& SA, int K) {
		int n = (int)SA.size();
		getBuckets(s, K, true);
		rer(i, n, 0) {
			int j = SA[i] - 1;
			if(j >= 0 && t[j]) SA[--bkt[s[j]]] = j;
		}
	}
	vi get_sa(const vi& s, int K) {
		int n = (int)s.size();
		vi t(n, 0);
		t[n - 1] = 1; t[n - 2] = 0;
		rer(i, n - 2, 0) 
			if(s[i] < s[i + 1] || (s[i] == s[i + 1] && t[i + 1] == 1)) t[i] = 1;
		getBuckets(s, K, true);
		vi SA(n, -1);
		rep(i, 1, n) if(isLMS(t, i)) SA[--bkt[s[i]]] = i;
		induceSAl(t, s, SA, K); induceSAs(t, s, SA, K);
		int n1 = 0;
		rep(i, 0, n) if(isLMS(t, SA[i])) SA[n1++] = SA[i];
		fill(SA.begin() + n1, SA.end(), -1);
		int name = 0, prev = -1;
		rep(i, 0, n1) {
			int pos = SA[i]; bool diff = false;
			rep(d, 0, n) {
				if(prev == -1 || s[pos + d] != s[prev + d] || t[pos + d] != t[prev + d]) {
					diff = true; break;
				}
				else if(d > 0 && (isLMS(t, pos + d) || isLMS(t, prev + d))) break;
			}
			if(diff) { name++; prev = pos; }
			pos = (pos % 2 == 0) ? pos / 2 : (pos - 1) / 2;
			SA[n1 + pos] = name - 1;
		}
		vi s1(n1, -1), SA1(n1, -1), p1(n1, -1);
		int at = 0;
		rep(i, n1, n) if(SA[i] >= 0) s1[at++] = SA[i];
		if(name < n1) SA1 = get_sa(s1, name);
		else rep(i, 0, n1) SA1[s1[i]] = i;
		getBuckets(s, K, true);
		at = 0;
		rep(i, 1, n) if(isLMS(t, i)) p1[at++] = i;
		fill(all(SA), -1);
		rer(i, n1, 0) {
			int j = p1[SA1[i]];
			SA[--bkt[s[j]]] = j;
		}
		induceSAl(t, s, SA, K); induceSAs(t, s, SA, K);
		return SA;
	}
	void get_lcp() {
		lcp.resize(n);
		int h = 0; lcp[0] = 0;
		rep(i, 0, n) {
			int j = sa[rank[i] - 1]; if(h > 0) h--;
			while(j + h < n && i + h < n && S[j + h] == S[i + h]) h++;
			lcp[rank[i] - 1] = h;
		}
	}
	int common(int i, int j) {
		if(i == j) return n - i;
		else {
			int a = rank[i], b = rank[j];
			if(a > b) swap(a, b);
			return seg.get(a, b).v;
		}
	}
};

sa.common(i,j)でsuffix(i)とsuffix(j)について求めることが出来る。
これを使うと色々面白いことが出来る。

まずはMP法と同じ配列を求めてみよう。

vi A(N + 1, -1);
rep(i, 1, N + 1) {
	int c = sa.common(0, i);
	rep(j, 0, c + 1) MAX(A[i + j], j);
}

suffix(0)とsuffix(i)がc文字一致していたら、suffix(0)とsuffix(i+j)はc-j文字一致していることを利用して求めている。
とてもシンプルだがO(N^2)。本家にはかなわない。

次は最長奇数回文

string rev = str; reverse(all(rev));
str = str + '{' + rev;
SA sa1; sa1.init(str_to_vec(str), 128);
rep(i, 0, N) {
	int at = 2 * N - i;
	debug(i, sa1.common(i, at) * 2 - 1);
}

真ん中の文字を決め打って、そこから何文字右方向と左方向に一致しているか見る。これはS=(元の文字列)+'{'+(反転させた文字列)についてSAをとればできる。最長偶数回文も同じようなことをすればできる。{は文字コード的にzの次なので便利。

KMPやMPは先頭とsuffix(i)が何文字一致しているかを求めるようなものだがSA+LCP+RMQなら任意の二つのsuffixについて求められる。
強い。(確信)

SA-IS

https://local.ugene.unipro.ru/tracker/secure/attachment/12144/Linear+Suffix+Array+Construction+by+Almost+Pure+Induced-Sorting.pdf
elementi-bioinformatica/Two Efficient Algorithms for Linear Time Suffix Array Construction.pdf at master · AlgoLab/elementi-bioinformatica · GitHub

SA-ISの論文2つだが、2つめのほうがわかりやすいし、実装例もあるので圧倒的にこっちをオススメする。

他にも役立ちそうなサイトをあげとく。
https://topcoder.g.hatena.ne.jp/iwiwi/20110720/1311168147
http://shogo82148.github.io/homepage/memo/algorithm/suffix-array/sa-is.html
http://blog.beam2d.net/2011/08/suffix-array-sa-is.html
http://gasin.hatenadiary.jp/entry/2017/06/08/101525

大まかなアルゴリズムの手順は、

1. Induced Sortを使ってLMS-substringをsortする。
2. LMS-substringのindexに関するSAを構築する。
3. 構築したSAを使って求めたい文字列に対するSAを構築する。

まずSuffix Arrayアルゴリズムに関する前提知識がないとInduced Sortで引っかかる。Induced Sortもかなり高度なことをやっていて論文だけでは到底理解できない。
LMSとついている言葉が大量に出てきて(LMS-substring, LMS-suffix, LMS-prefix)これも混乱を招く要因。

論文を読むのは初めてではないといえどかなり苦戦した。
次からは言葉の定義をちゃんと抑えながら、適宜ググるのも忘れず、読み進めるようにしたい。

以下自分で実装したSA-IS(論文のコードを扱いやすくしただけだが)

namespace SA { //please init, don't use 0 as character
	vi bkt;
	inline bool isLMS(const vi& t, int i) {
		if(i > 0 && t[i] && !t[i - 1]) return true;
		else return false;
	}
	void getBuckets(const vi& s, int K, bool end) {
		int sum = 0, n = (int)s.size();
		fill(bkt.begin(), bkt.begin() + K, 0);
		rep(i, 0, n) bkt[s[i]]++;
		rep(i, 0, K) {
			sum += bkt[i];
			bkt[i] = end ? sum : sum - bkt[i];
		}
	}
	void induceSAl(const vi& t, const vi& s, vi& SA, int K) {
		int n = (int)SA.size();
		getBuckets(s, K, false);
		rep(i, 0, n) {
			int j = SA[i] - 1;
			if(j >= 0 && !t[j]) SA[bkt[s[j]]++] = j;
		}
	}
	void induceSAs(const vi& t, const vi& s, vi& SA, int K) {
		int n = (int)SA.size();
		getBuckets(s, K, true);
		rer(i, n, 0) {
			int j = SA[i] - 1;
			if(j >= 0 && t[j]) SA[--bkt[s[j]]] = j;
		}
	}
	vi get_sa_impl(const vi& s, int K) {
		int n = (int)s.size();
		vi t(n, 0);
		t[n - 1] = 1; t[n - 2] = 0;
		rer(i, n - 2, 0) 
			if(s[i] < s[i + 1] || (s[i] == s[i + 1] && t[i + 1] == 1)) t[i] = 1;
		getBuckets(s, K, true);
		vi SA(n, -1);
		rep(i, 1, n) if(isLMS(t, i)) SA[--bkt[s[i]]] = i;
		induceSAl(t, s, SA, K); induceSAs(t, s, SA, K);
		int n1 = 0;
		rep(i, 0, n) if(isLMS(t, SA[i])) SA[n1++] = SA[i];
		fill(SA.begin() + n1, SA.end(), -1);
		int name = 0, prev = -1;
		rep(i, 0, n1) {
			int pos = SA[i]; bool diff = false;
			rep(d, 0, n) {
				if(prev == -1 || s[pos + d] != s[prev + d] || t[pos + d] != t[prev + d]) {
					diff = true; break;
				}
				else if(d > 0 && (isLMS(t, pos + d) || isLMS(t, prev + d))) break;
			}
			if(diff) { name++; prev = pos; }
			pos = (pos % 2 == 0) ? pos / 2 : (pos - 1) / 2;
			SA[n1 + pos] = name - 1;
		}
		vi s1(n1, -1), SA1(n1, -1), p1(n1, -1);
		int at = 0;
		rep(i, n1, n) if(SA[i] >= 0) s1[at++] = SA[i];
		if(name < n1) SA1 = get_sa_impl(s1, name);
		else rep(i, 0, n1) SA1[s1[i]] = i;
		getBuckets(s, K, true);
		at = 0;
		rep(i, 1, n) if(isLMS(t, i)) p1[at++] = i;
		fill(all(SA), -1);
		rer(i, n1, 0) {
			int j = p1[SA1[i]];
			SA[--bkt[s[j]]] = j;
		}
		induceSAl(t, s, SA, K); induceSAs(t, s, SA, K);
		return SA;
	}
	vi get_sa(const vi& sin, int K = 26) {
		int n = (int)sin.size();
		vi s(n + 1, 0);
		rep(i, 0, n) s[i] = sin[i] + 1;
		bkt.resize(max(n + 1, K + 1));
		return get_sa_impl(s, K + 1);
	}
}

無理やり圧縮しまくって81行になった。一応POJ3581でverify済み。
番兵とかも気を遣わず、元の文字列を数字に変換したのをそのままget_saの引数にすればSAが得られるようにした。
namespaceよりもstructにする方がいい気もするが、今のところnamespaceで困らないのでこれで。
(9/2追記)

rer(i, n1, 0) {
	int j = p1[SA1[i]]; //int j = p1[i]としてバグらせていた。
	SA[--bkt[s[j]]] = j;
}
induceSAl(t, s, SA, K); induceSAs(t, s, SA, K);

8/31にあげたコードがバグっていたので修正。なのになぜかPOJ3581は通ったんだよなぁ…。
デバッグの際にgasin氏のSAを活用させてもらった。gasinありがとー!

AOJ2230: How to Create a Good Game

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2230

LP双対の問題。似たような問題でLongest Shortest Pathがあるのでそれの解説スライドを見ながら考察した。

http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=2736


まず最長距離の双対を取ってポテンシャルに言い換える。
すると条件は
max (Σx)
s.t.
d[t]-d[s]<=D (双対変数F)
d[u]-d[v]+xe<=-le (for all e=(u,v)) (双対変数fe)
となる。(新しくdという変数を追加するのがポイント)

これの双対をとると
min Σ(-ce*fe)+D*F
(sから出る辺のfeの合計)>=F ...(1)
(vから出る辺のfeの合計)-(uに入る辺のfeの合計)>=0 ...(2)
(tに入る辺のfeの合計)<=F ...(3)
fe >= 1 ...(4)

(2)について考えると、各点で水が足される可能性があるとみてよい。
なのでF'(>=F)だけsから流されたとすると、tはF''(>=F')だけ水を受け取ることになる。
しかし、(3)の条件よりF''=F'=Fとなり、
(1)(2)(3)<=>
(sから出る辺のfeの合計)=F
(vから出る辺のfeの合計)-(uに入る辺のfeの合計)=0
(tに入る辺のfeの合計)=Fとなり、これは流量条件である。

(4)と合わせてこの問題は、
各辺1の最小流量制約があるグラフにおいて、M=(Fだけ水を流したときのコスト)+D*Fを最小化する問題に言い換えられた。

Fを固定すれば
(Fだけ水を流したときのコスト)については最小費用流で最小化すればよい。

また流量FのフローはF本のsからtへのpathに分解できることに留意すると、
流量をさらに1増やしたとき新しくpathが追加されるが、その長さはDより小さい。
よってFを増やすとMは必ず大きくなるので、F=(すべての辺をpathでカバーする際の本数の最小値)となる。これはにぶたんで求めてもいいし、sとtに辺を張って一発で求めることもできる。
にぶたんのupper_boundをMではなくNにしてしまいこれのせいで時間を無限に溶かした。過去の自分のブログを見返したら同じミスをしていて笑った。

また上の式を見てもらえばわかるが、最小費用流の双対は
d[u]-d[v]+xe<=leのような形をしていることがわかる。
http://tokoharuland.hateblo.jp/entry/2016/12/06/223614
を見て貰えばわかるが、最大流量の制約も追加すると式はd[u]-d[v]+xe-ye<=leのようになる。3or4変数の式がたくさん出てきたら最小費用流の双対を疑うべき。


下はsからtへ辺を張る方法でFを求めている。

int N, M, F;
int A[MAX_N], B[MAX_N], C[MAX_N];
int S[MAX_N];

void solve() {
	cin >> N >> M;
	MCF::init(N);
	rep(i, 0, M) {
		cin >> A[i] >> B[i] >> C[i];
		MCF::add_edge(A[i], B[i], inf, 0);
		MCF::add_edge(A[i], B[i], 1, -2);
	}
	MCF::add_edge(0, N - 1, inf, -1);
	int f = MCF::get(0, N - 1, M) + 3 * M;

	int res = 0;
	int s = N, t = N + 1;
	MCF::init(N + 2);
	S[0] = -f;
	S[N - 1] = f;
	rep(i, 0, M) {
		MCF::add_edge(A[i], B[i], inf, -C[i]);
		S[A[i]]++; S[B[i]]--;
		res += -C[i];
	}
	int d = MCF::get_longest(0, N - 1);
	F = 0;
	rep(i, 0, N) {
		if(S[i] > 0) MCF::add_edge(i, t, S[i], 0);
		if(S[i] < 0) {
			MCF::add_edge(s, i, -S[i], 0);
			F += -S[i];
		}
	}
	res += MCF::get(s, t, F);
	cout << res + d * f << "\n";
}

高難易度の問題はWA出すと本質が間違っているのではないかと疑いがち。自信を持ってください。

最短距離問題の双対について

次のグラフの最短距離問題のLP考えよう。
f:id:omochangram:20170826154614p:plain

辺は(辺番号)/(辺のコスト)となっている。
LPは次のようになる。

min(y1+3(y2)+y3+3(y4)+y5)
s.t.
y1+y2=1
y1-y3-y4=0
y2+y3-y5=0
y4+y5=1
y1,y2,y3,y4,y5>=0

行列を使うと、
min{yb|y>=0, yA>=c}となる。ただし
A=
{{ 1,-1, 1,-1, 0, 0, 0, 0},
{ 1,-1, 0, 0, 1,-1, 0, 0},
{ 0, 0,-1, 1, 1,-1, 0, 0},
{ 0, 0,-1, 1, 0, 0, 1,-1},
{ 0, 0, 0, 0,-1, 1, 1,-1}}
b={1,3,1,3,1}^t
c={1,-1,0,0,0,0,1,-1}
y={y1,y2,y3,y4,y5}

y1,y2,y3,y4,y5={0, 1}の条件はいらないのかと思うが、Aは完全単模性を有しており、解が整数になることを示せる。詳しくは
http://www.misojiro.t.u-tokyo.ac.jp/~murota/lect-surikeikakuhou/integerprogunimod101222.pdf
を参照。
解が0以上の整数なので、グラフに負のループが存在しないと仮定すればyの値は確かに0または1になることがわかる。

では双対をとってみると、
max{cx|x>=0,Ax<=b}<=>

max( (x1-x2)+(x7-x8) )
s.t.
(x1-x2)+(x7-x8)<=1
(x1-x2)+(x5-x6)<=3
-(x3-x4)+(x5-x6)<=1
-(x3-x4)+(x7-x8)<=3
-(x5-x6)+(x7-x8)<=1
ここで
z1=-(x1-x2)
z2=(x3-x4)
z3=(x5-x6)
z4=(x7-x8)と置けば

max(z4-z1)
s.t.
-z1+z2<=1
-z1+z3<=3
-z2+z3<=1
-z2+z4<=3
-z3+z4<=1

s.t.以下の形はz1~z4を1~4の点としてみた時のpotentialの定義に他ならない。
なので最短距離問題は最大potential問題(?)に言い換えることが出来た。

最短距離問題を解くことによってpotentialが求められることがわかり、最小費用流のpotentialの初期化もこれで行えるようになった。

最大potential問題のことを競プロ界隈では牛ゲーという。

LPを解こうとして2変数の式が大量に出てきたときは牛ゲーを疑うと良い。

ちなみに最大フローの双対は最小カットだが、最小カットはあまり式にしてもうれしくなさそう。
集合を2つに分ける問題で使えるかも位の認識でいいと思う。