★4~5位の手抜き解法です。
http://yukicoder.me/problems/874
問題
N種類の文字がある言語がある。
2文字の単語はN*N通り考えられるが、うちM個辞書に載っていない単語がある。
辞書に載っている(N*N-M)個の単語をすべて1回ずつ使うしりとりをしたい。
単語順は何通り考えられるか。
解法
頂点を文字、単語を2つの文字をつなぐ有向辺としよう(同じ文字を2つ使う場合、ループも存在する)。
この問題は、全ての辺を1回ずつ辿るオイラー路の組み合わせ数を求める問題となる。
各頂点の入次数・出次数がすべて等しい(かつ全辺が連結)なら、このオイラー路はオイラー閉路となる。
また、入次数・出次数のどちらかが1多い頂点が1つずつあれば、このオイラー路の始点終点は確定する。
オイラー閉路の組み合わせ数の計算方法はすでに手法があるため、後者もオイラー閉路として扱いたい。
そこで、与えられたグラフが後者なら、(入次数の方が1多い点)から(出次数の方が1多い点)に1本辺を追加し、全点入次数と出次数が等しくなるようにしよう。
最初の1手はその追加した辺に固定したオイラー閉路を考えることは、辺を追加する前のグラフでオイラー路を考える事に等しくなる。
あとはこのオイラー閉路の組み合わせ数を求める問題に還元できる。
Editorialにもあるとおり、オイラー閉路の組み合わせ数はWikipediaに書いてある。
BEST theorem - Wikipedia
式は同じだが、上記定理の説明は以下のサイト(PDF)の方がわかりやすかった。
http://www.theoremoftheday.org/CombinatorialTheory/BEST/TotDBEST.pdf
さて、このこのBEST Theoremの計算過程では全域木の数を求める必要があり、それにはラプラシアン行列から1行1列削った行列の行列式が必要になる。
愚直に掃出し法を行うとこれはO(N^3)かかりTLEする。
そこでEditorialでは"black box linear algebra"という手法を紹介している(自分はまともに見てないし理解もしていない)。
ただ、このラプラシアン行列は対角成分以外はほとんど-1である。
よって掃出し法を行う際、適当に行を入れ替えると計算過程で多くの成分が0になり、大幅に高速化できる。
以下のコードでは、掃出し法の過程で乱択で行を入れ替えたところ、何とか通った。
なお、このBEST theoremで求められるのはオイラー閉路の数だけである。
実際にはこの問題のケースでは「どの辺から始まるか」を考慮する必要があるので、入次数・出次数がすべて等しい場合は辺の数である(N*N-M)倍する必要がある。
入次数・出次数のどちらかが1多い頂点が1つずつあるケースは最初の辺を固定しているので、(N*N-M)倍は必要ない。
int N,M; int A[4040],B[4040]; int in[4040],out[4040]; int st,en; ll fact[8080]; ll mo=1000000007; int id[4040],rid[4040]; int edge[4040][4040]; ll mat[4040][4040]; ll modpow(ll a, ll n = mo-2) { ll r=1; while(n) r=r*((n%2)?a:1)%mo,a=a*a%mo,n>>=1; return r; } ll det_mo(int N) { int x,y,z; ll ret=1; FOR(y,N) FOR(z,N) mat[y][z]=((mat[y][z]%mo)+mo)%mo; FOR(x,N) { vector<int> V; for(y=x;y<N;y++) if(mat[y][x]) V.push_back(y); if(V.size()==0) return 0; y=V[rand()%V.size()]; if(y!=x) { FOR(z,N) swap(mat[x][z],mat[y][z]); ret=(mo-ret)%mo; } ret=ret*mat[x][x]%mo; ll rev=modpow(mat[x][x]); FOR(z,N) mat[x][z]=rev*mat[x][z]%mo; for(y=x+1;y<N;y++) if(mat[y][x]) { rev=mat[y][x]; for(z=x;z<N;z++) if(mat[x][z]) mat[y][z]=(mat[y][z]+mo*mo-mat[x][z]*rev)%mo; } } return ret; } void solve() { int i,j,k,l,r,x,y; string s; srand(time(NULL)); fact[0]=1; FOR(i,8000) fact[i+1]=fact[i]*(i+1)%mo; cin>>N>>M; FOR(i,N) { FOR(j,N) edge[i][j]=-1; in[i]=out[i]=N; edge[i][i]=N-1; } FOR(i,M) { cin>>x>>y; x--,y--; out[x]--; in[y]--; edge[x][y]++; edge[y][y]--; } x=0; st=en=-1; FOR(i,N) { if(in[i]==0 && out[i]==0) id[i]=-1; else id[i]=x, rid[x]=i, x++; if(abs(in[i]-out[i])>1) return _P("0\n"); if(in[i]==out[i]+1) { if(en!=-1) return _P("0\n"); en=i; } if(out[i]==in[i]+1){ if(st!=-1) return _P("0\n"); st=i; } } if(x==0 || x==1) return _P("1\n"); ll pat=(N*N-M)%mo; N=x; if(st>=0) { // odd in[st]++; out[en]++; edge[en][st]--; edge[st][st]++; pat=1; } FOR(i,N) pat = pat*fact[in[rid[i]]-1]%mo; FOR(i,N) FOR(j,N) mat[i][j]=edge[rid[i]][rid[j]]; cout<<(pat*det_mo(N-1)%mo)<<endl; }
まとめ
ひどい手抜き解答ではありますが…なんとか新年発yukicoderの前に全完に戻せました。