公式

G - Only One Product Name 解説 by mechanicalpenciI


まず、次のような有向グラフ \(G\) を考えます。

  • \(G\) の頂点はすでに使用されている商品と一対一に対応する。
  • (相異なるとは限らない)\(G\) の頂点 \(u\), \(v\) について、\(u\) に対応する商品名の \(2\) 文字目と \(v\) に対応する商品名の \(1\) 文字目が一致するとき、かつその時に限り頂点 \(u\) から頂点 \(v\) へ有向辺を張る。

このとき、NGリストの各文字列は \(G\) 上の歩道と対応させることができます。

NGリストの文字列と $G$ 上の歩道の対応 NGリストの文字列 $S$ について、$S$ の長さを $|S|$, $i$ 文字目を $S_i$ としたとき、$S_1S_2$, $S_2S_3$, $\ldots$, $S_{|S|-1}S_{|S|}$ はそれぞれすでに使用されている商品名であるため、それぞれに対応する頂点を並べたものが $G$ 上の歩道となります。ここで、$G$ の定義より、$S_{i-1}S_i$ から $S_iS_{i+1}$ $(2\leq i\leq |S|-1)$ へつねに辺が貼られていることに注意してください。 逆に歩道から文字列を構成することもできます。

このとき、元の問題は、「 \(G\) 上の歩道の組であって、\(G\) の任意の頂点が \(1\) つ以上の歩道に含まれているものについて、組に含まれる歩道の数としてあり得る最小の値を求めよ。」と言い換えることができます。

次に \(G\) について強連結成分分解を行い、それをもとにした次の有向グラフ \(G'\) を考えます。

  • \(G'\) の頂点は \(G\) の強連結成分と一対一に対応する。
  • \(G'\)相異なる頂点 \(u'\), \(v'\) についてそれぞれに対応する強連結成分に属する \(G\) 上の頂点 \(u,v\) について\(u\) から \(v\)\(G\) 上で(\(1\) 本以上の辺を通って)到達可能であるとき、かつそのときに限り \(u'\) から \(v'\) へ有向辺を張る。ここで、\(u, v\) の選び方は強連結成分に対して一意でないが、強連結成分の定義より、到達可能であるかは一意であることに注意する。

このとき、\(G'\) における最小道被覆の有向道の数が問題の答えと一致することが証明できます。
ここで、グラフ \(G'\) 上の道被覆とは\(G'\) 上の有向道の集合であって、\(G'\) 上のすべての頂点がそのうちのちょうど \(1\) つ(の有向道)に含まれるようなものをさし、最小道被覆は道被覆のうち、要素数(属する有向道の数)が最小となるものをさします。

(NGリストに含まれる文字列の数の最小値)$\leq$($G'$ における最小道被覆の要素数)であることの証明 $G'$ における最小道被覆を$1$ つ取ります。ここで、次の $2$ 点に注意します。
まず、$G'$ 上の各頂点について、対応する$G$ の強連結成分に含まれる頂点を全て通るような歩道を構成することができます。これは強連結成分の定義より可能です。
次に、$G'$ 上において $u'$ から $v'$ に辺が張られているとき、それぞれに対応する強連結成分に属する任意の頂点の組 $(u,v)$ について、始点 が$u$ であり終点が $v$ であるような歩道を構成することが可能です。これは $G'$ の辺の定義より従います。
これらを組み合わせることによって、$G'$ 上の有向道から、$G$ 上の歩道であって、$G'$ 上の有向道に含まれる頂点に対応する全ての強連結成分に属する全ての($G$ 上の)頂点を通るものが構成できることがわかります。これを最小道被覆に属する各有向道について行った後の集合は「 $G$ 上の歩道の組であって、$G$ の任意の頂点が $1$ つ以上の歩道に含まれている」という条件をみたしているため、
(NGリストに含まれる文字列の数の最小値)
$=$( $G$ 上の歩道の組であって、$G$ の任意の頂点が $1$ つ以上の歩道に含まれているものについて、組に含まれる歩道の数の最小値)
$\leq$($G'$ における最小道被覆の要素数)
が成り立つことがわかります。
(NGリストに含まれる文字列の数の最小値)$\geq$($G'$ における最小道被覆の要素数)であることの証明 NGリストに含まれる文字列の数の最小となるようなNGリストの文字列の集合を考え、それに対応する$G$ 上の歩道の集合をとります。
この歩道を $w_1,w_2,\ldots,w_k$ とし、$w_i$ の頂点の列を $(v_{i,0},v_{i,1},\ldots,v_{i,|w_i|})$ とします。ここで、$|w_i|$ は $w_i$ の長さです。(頂点数は長さより $1$ 大きいことに注意してください。)
このとき、$v_{1,0},v_{1,1},\ldots,v_{1,|w_1|},v_{2,0},v_{2,1},\ldots, v_{2,|w_2|}, \ldots, v_{k,0},v_{k,1},\ldots, v_{k,|w_k|}$ には$G$ 上の全ての頂点が 一度以上ずつ含まれています。この順に頂点を見ていき、$G'$ 上の各頂点について、その頂点に対応する強連結成分に属する頂点のうち最初に登場したものに印をつけます。ここにおける印は(道, 頂点)の組に対してつけるものとし、同じ頂点であってもその後に登場するものには付けないものとします。
さて、$(v_{i,0},v_{i,1},\ldots,v_{i,|w_i|})$ について印の付けられた頂点が $x$ 個あり、それぞれ $G'$ 上の頂点 $u_1,u_2,\ldots,u_x$ に対応する強連結成分に属する頂点のうち最初に登場したものであったとき、定義より $(u_1,u_2,\ldots,u_x)$ は $G'$ 上の道となります。$w_1,w_2,\ldots,w_k$ のうち印が付けられた頂点が $1$ つ以上存在するものについてこのように構成した $G'$ 上の道を集めた集合を考えると、$G'$ の任意の頂点について対応する印のついた(道, 頂点)の組がちょうど $1$ つ存在するため、これは$G'$ の辺被覆となります。
よって、$G'$ の最小辺被覆の要素数は $k$ 以下であり、
(NGリストに含まれる文字列の数の最小値)
$=$( $G$ 上の歩道の組であって、$G$ の任意の頂点が $1$ つ以上の歩道に含まれているものについて、組に含まれる歩道の数の最小値)
$\geq$($G'$ における最小道被覆の要素数)
となります。

よって、\(G'\) 上での最小道被覆問題を解けば良いです。
ここで、\(G'\)\(G\) を強連結成分分解した後のグラフについて、(自己ループ及び多重辺を取り除き)到達可能な頂点の間に新たに有向辺を張ったものであるので、\(G'\) はDAG となっています。 DAG上の最小道被覆問題は二部グラフの最大マッチングを求めることによって解くことができます。
具体的には、\(G'\)の頂点数を \(N'\) として、\(2\) つの \(N'\) 頂点からなる集合 \(U=\{U_1,U_2,\ldots,U_{N'}\}\), \(V=\{V_1,V_2,\ldots, V_{N'}\}\) を考え、\(G'\) 上で頂点 \(u\) から頂点 \(v\) への辺が存在するとき(かつそのときに限り)\(U_u\)\(V_v\) を結んだときの \(U,V\) 間の最大マッチングの数を \(N'\) から引いたものが答えとなります。

アルファベットの数を\(K=26\) として、\(G\) の頂点数は高々 \(K^2\), 辺の数は高々 \(K^3\) であり、 \(G'\) の頂点数は高々 \(K(K+1)/2\), 辺の数は \(K^2(K+1)^2/4\) です。(\(G'\) の辺数は実際はもっと厳しく\((K+2)^4/24\)などで評価できます) このとき、\(G\) の構成および強連結成分分解には高々 \(O(K^3)\), \(G'\) に対応する二部グラフの構成は愚直に行えば \(O(K^6)\), 二部グラフの最大マッチングを求めるには \(O(K^{16/3})\) の計算量がかかります 。ここで、\(K^6\simeq 3\times 10^8\) ですが、実際には頂点数や辺数はそれらの\(1/2\)\(1/4\) 以下であるため十分高速です。
よって、この問題を解くことができました。

c++ による実装例:

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

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

int main(void){
	int n,m,sz;
	string s[N];
	vector<vector<int> > scc;
	int grp[N];
	bool r[N][N];

	cin>>n;
	rep(i,n)cin>>s[i];
	scc_graph g1(n);
	rep(i,n)rep(j,n){
		if(s[i][1]==s[j][0])g1.add_edge(i,j);
	}
	scc=g1.scc();
	m=scc.size();
	rep(i,m){
		sz=scc[i].size();
		rep(j,sz){
			grp[scc[i][j]]=i;
		}
	}

	rep(i,n)rep(j,n){
		if(s[i][1]==s[j][0]){
		  r[grp[i]][grp[j]]=true;
		}
	}
	for(int i=m-1;i>=0;i--){
		for(int j=i+1;j<m;j++){
			if(r[i][j]){
				for(int k=j+1;k<m;k++){
					r[i][k]|=r[j][k];
				}
			}
		}
	}

	mf_graph<int> g2(2*m+2);
	for(int i=0;i<m;i++){
		g2.add_edge(0,i+1,1);
		g2.add_edge(i+m+1,2*m+1,1);
	}
	for(int i=0;i<m;i++){
		for(int j=i+1;j<m;j++){
			if(r[i][j])g2.add_edge(i+1,j+m+1,1);
		}
	}

	cout<<(m-g2.flow(0,2*m+1))<<endl;
	return 0;
}

投稿日時:
最終更新: