POJ-2085: Inversion
こんなに面倒なはずがない、という問題だったのでいつもより丁寧に書く
keyword
反転数 Greedy BIT 二分探索 C++
問題概要
[1..N](N<5*10^4)のpermutationのうち、反転数がMになる辞書順最小のものを出力する問題。
素朴な解法
辞書順最小という問題が出たら解法は大体2つ。
- 条件を満たすものは全部作り出すことができる。つまり辞書順最小は本質ではない。
- 前からGreedyに詰めていく。
(追記:解の候補を辞書順に小さい方から生成してアタリが出るまで試す、という解法も考えられる。広い意味ではこれは前者に含まれるかもしれないが)
今回の場合は明らかに後者だろう(少し丁寧に考えると、N!をMで割った値が大きすぎて前者が無理だと分かる)。
まず反転数の上限について考えてみよう。長さnの数列の反転数の上限はn*(n-1)/2である([N..1]のとき)。
となると、次のように解けば良いことが分かる。
- (N-1)*(N-2)/2>=Mなら先頭にはまだ使ってない数のうち最小のものを置いて良い。
- (N-1)*(N-2)/2
- あとは残りのリストについて再帰的に解く。
これを素直に実装したのがこれ。
int64 N, M; void solve(){ vector<int> ans; set<int> unused; for(int i=1;i<=N;i++) unused.insert(i); for(int i=0; i<N; i++){ int64 res = N - i - 1, most = res*(res-1)/2; set<int>::iterator itr = unused.begin(); if(most < M){ int pos = M - most; M -= pos; advance(itr,pos); } ans.push_back(*itr); unused.erase(itr); } for(int i=0; i<N; i++) printf("%d%c",ans[i], i==N-1?'\n':' '); } int main(){ while(scanf("%lld%lld",&N,&M),N+1) solve(); return 0; }
実はこれだと計算量がO(N^2)になっていてTLEする。何故かというとadvance(itr, pos)に最悪O(N)かかってしまうから。
改善案
advanceに時間が掛かっていたのがTLEの原因なだけで、部分問題を残して元の問題は殆ど解けている。
上の解法の過程で、「i番目(0-origin)にはまだ使ってない数のうちa[i]番目の数を使う」という情報はO(N)で得られることが分かる。この情報を使って答えとなる配列を構成しないといけない。この新しい部分問題について考えよう。
この問題は、「1,2,...,K-1のうち既に使ったものがi個以上となる最小のK」を求めれば良い。これはBITを使った二分探索で求められる。
この解法だと計算量はO(N * (log N)^2)。一応ACだった。
int64 N, M; int bit[MAX_N]; vector<int> adds, ans; void initBIT(){ memset(bit,0,sizeof(bit)); } int sum(int n){ int ret=0;for(;n;n-=n&-n)ret+=bit[n];return ret; } int add(int n, int x){ for(;n<=N;n+=n&-n)bit[n]+=x; } void solve(){ adds.clear(); ans.clear(); for(int i=0; i<N; i++){ int64 res = N - i - 1, most = res*(res-1)/2; if(most < M){ int pos = M - most; M -= pos; adds.push_back(pos); } else{ adds.push_back(0); } } subsolve(); for(int i=0; i<N; i++) printf("%d%c",ans[i], i==N-1?'\n':' '); } bool check(int mid, int val){ return sum(mid)>val; } void subsolve(){ initBIT(); for(int i=1; i<=N; i++) add(i,1); for(int i=0; i<N; i++){ int high=N, low=0; while(high-low>1){ int mid = (high + low)>>1; if(check(mid, adds[i])) high = mid; else low = mid; } ans.push_back(high); add(high,-1); } } int main(){ while(scanf("%lld%lld",&N,&M),N+1) solve(); return 0; }
感想
どう考えてもこんな面倒くさい問題なはずがない。嘘解法臭がきつすぎる。
どうしても分からなかったときは実験してみるに限る…だけど眺めてみても綺麗な法則があるようには見えない。どう解くのがスマートなんだろう。