Xmas Contest 2016 H解説

H問題の解法と雑な証明です。

まず、計算量無視して貪欲法を考える。
根付き木にして、
最も深い頂点から順に、距離X以内に頂点がないなら取るというのを繰り返す。
そうすると最適解が作れる。
証明
深い順に頂点に昇順に番号をつける。vの番号をf(v)とする。
上記のアルゴリズムで最適解が得られないと仮定する。
上記のアルゴリズムで得られる解において選ぶ頂点の集合をSとする
すべての最適解において選ぶ頂点の集合をTとしたとき、「条件:頂点x,yであって、f(x)<f(y) かつ x∈S かつ x∉T かつ y∈T、となるものが存在する」を満たすはずである。
そのようなx,yであって、f(x)が最小であって、さらにそのうちf(y)が最小であるものをとってくると、yを選ばないことにしてxを選ぶことによって別の最適解が得られ、これを繰り返していくと「条件」を満たさないような最適解が得られることになり、矛盾する。
「yを選ばないことにしてxを選ぶこと」が常に可能であることを証明する必要がある。
f(x)が最小であるから少なくともf(v)<f(x)となるvについてはSとTで状況が同じであるため、xを選ぶ際の障害にはならない。f(y)が最小であることから、f(x)<f(v)<f(y)となるvは選ばれていないはずである。f(y)<f(v)かつv∈Tとなるvに関しては、dist(x,y)<X≦dist(v,y)であるはずで、「f(x)<f(y)<f(v)」「dist(x,y)<X≦dist(v,y)」からX≦dist(v,y)<dist(v,x)が言えるため、障害にならない。
で、また計算量を無視してDFSで集合S(証明内で定義したやつ)を求めてみようとしてみる。
部分木iだけみたときに選ばれる集合をS_iとする。
部分木i内の頂点vがv∈Sならばv∈S_iとなることは明らか。
S_iとS_jをマージするときは、集合に含まれる頂点を深い順に見て、選べるものだけ選ぶとよい。
S_iの性質として「根から距離X/2以下の頂点は高々1つしかない」ということが言える(もし2つ以上あるなら明らかにその2頂点の距離はX以下であるため)。そのような頂点をSa_iと呼ぶことにする。
Sa_iとSa_jの距離も明らかにX以下なので、少なくともどちらかは捨てなければならない。
また、Sa_i,Sa_j以外の頂点はマージした後も全て選ぶことが出来る事も言える。根からの距離がX/2より大きいため、それらの距離はXよりも大きくなるためである。
Sa_iとSa_jの話に戻るが、このうち深い方以外は必ず捨てることになる。f(Sa_i)<f(Sa_j)とすると、Sa_iが選べずSa_jが選べるというケースはありえないためである。
つまりマージの際行うべきことは、以下の通り。
Sa_i,Sa_j以外の頂点を全て採用する。
Sa_i,Sa_jのうち最も深いものをSaとする。
Saから距離X以内の頂点がない場合はSaを採用する。
ここで、「Saから距離X以内の頂点」となりうるものは「根から距離X/2以下の頂点」が存在しない部分木の頂点でもっとも根に近いものしかない点に注意したい。
マージすべき部分木が3つ以上の場合も上と同様の議論ができる。
上記に基いて効率的なアルゴリズムを設計する。
dfsをして、各部分木で選んだ頂点のうち最も根に近いもの(と選んだ頂点の個数)を求めていく。
ある頂点v以下の部分木に関して計算するときは、その子の部分木内で最も根に近いものを深さ順にソートし、深いものから順に選べる限り選んでいく。
選んだもののうち最も根に近いものをdfsの戻り値として返す。
かなりシンプルなアルゴリズムになった。


しかしまだソートのlogがついているので改善する。
実は、マージのときには「X/2以下のもののうちの最大値」と「X/2より大のもののうちの最小値」を求めていくだけでいい。

open all / close all


generated by indenter


以下ソースコードです。マクロ多くてごめんなさい。

#include略
#define fi first
#define se second
#define rep(i,n) for(int i = 0; i < (n); ++i)
#define rrep(i,n) for(int i = 1; i <= (n); ++i)
#define drep(i,n) for(int i = (n)-1; i >= 0; --i)
#define gep(i,g,j) for(int i = g.head[j]; i != -1; i = g.e[i].next)
#define each(it,c) for(__typeof((c).begin()) it=(c).begin();it!=(c).end();it++)
#define rng(a) a.begin(),a.end()
#define maxs(x,y) x = max(x,y)
#define mins(x,y) x = min(x,y)
#define pb push_back
#define sz(x) (int)(x).size()
#define pcnt __builtin_popcount
#define uni(x) x.erase(unique(rng(x)),x.end())
#define snuke srand((unsigned)clock()+(unsigned)time(NULL));
#define df(x) int x = in()
#define dame { puts("-1"); return 0;}
#define show(x) cout<<#x<<" = "<<x<<endl;
#define PQ(T) priority_queue<T,vector<T>,greater<T> >
#define bn(x) ((1<<x)-1)
using namespace std;
typedef long long int ll;
typedef pair<int,int> P;
typedef vector<int> vi;
typedef vector<vi> vvi;
typedef vector<ll> vl;
typedef vector<P> vp;
inline int in() { int x; scanf("%d",&x); return x;}
inline void priv(vi a) { rep(i,sz(a)) printf("%d%c",a[i],i==sz(a)-1?'\n':' ');}
template<typename T>istream& operator>>(istream&i,vector<T>&v)
{rep(j,sz(v))i>>v[j];return i;}
template<typename T>string join(vector<T>&v)
{stringstream s;rep(i,sz(v))s<<' '<<v[i];return s.str().substr(1);}
template<typename T>ostream& operator<<(ostream&o,vector<T>&v)
{if(sz(v))o<<join(v);return o;}
template<typename T1,typename T2>istream& operator>>(istream&i,pair<T1,T2>&v)
{return i>>v.fi>>v.se;}
template<typename T1,typename T2>ostream& operator<<(ostream&o,pair<T1,T2>&v)
{return o<<v.fi<<","<<v.se;}
const int MX = 100005, INF = 1001001001;

int n, m;
vvi to, co;
int cnt;
int dfs(int v, int x, int p=-1) {
  int a = 0, b = INF;
  rep(i,sz(to[v])) {
    int u = to[v][i];
    if (u == p) continue;
    int r = dfs(u,x,v)+co[v][i];
    if (r*2 >= x) {
      mins(b,r);
    } else {
      --cnt;
      maxs(a,r);
    }
  }
  if (a+b < x) return b;
  ++cnt;
  return a;
}
int f(int x) {
  cnt = 0;
  dfs(0,x);
  return cnt;
}

int main() {
  scanf("%d%d",&n,&m);
  to = co = vvi(n);
  rep(i,n-1) {
    int a,b,c;
    scanf("%d%d%d",&a,&b,&c);
    --a; --b;
    to[a].pb(b); co[a].pb(c);
    to[b].pb(a); co[b].pb(c);
  }
  int l = 1, r = INF;
  while (l+1<r) {
    int c = (l+r)>>1;
    if (f(c) >= m) l = c; else r = c;
  }
  cout<<l<<endl;
  return 0;
}

Xmas Contest 2016

Xmas Contest 2016 にご参加いただいた皆様、ありがとうございました&お疲れ様でした。

難易度が高すぎて順位表が大変なことになっていましたね。。
1問をじっくり考えるのに慣れていない方には少しつらいセットだったかもしれないです。
でも、なんだかんだ出来ることはあって座るだけにはならなかった方が多く、その点は良かったかなぁと思っています。

それでは解説を...のつもりだったのですが、解説をガッツリ書く体力が残っていないため、ヒントだけで失礼いたします。

*追記:解説情報です。 ABCFHI

ヒント

A (原案:snuke)
N=15のとき、8,4,2,1と取っていくと4回ですが、a=[0,8),b=[7,15),c=[7,8)とすると3回の質問で総和(a+b-c)が求められる。
このように引き算を駆使すると、二進数での桁数/2回くらいの質問で総和が求められる。
ちなみに10回必要になる最小のNは二進数で11010101010101011です。
B (原案:snuke)
50点を超える最も簡単な解法は、スターンブロッコット木を、分母をx分子をyとしてそのまま埋め込む解法だと思います。
70~90点はフラクタルっぽい図形を頑張って構成すれば良いです。
満点は64*64に収めなければなりません。余白は1点しか許されません。
これは図を使って説明しないと行けないので後で。
C (原案:japlj)
二分探索すると01列を切ったり捨てたりして1を残したら勝ちというゲームになります。
0,1のかわりに-1,1で考えます。
累積和をとります。
全体の累積和が0でない場合、正なら勝ち、負なら負けです。
0の場合は、累積和が0となっているような切れ目の個数が奇数個なら勝ちです。(先に切る番の場合)
戦略は上記の結果を証明しようとすると自然と導かれます。
D (原案:japlj)
簡単枠のはずでした(自明ではないですが)が昼の部ではなかなか解かれませんでした。
各サイクルに注目すると、
サイズが2なら1回でいい。
サイズが3以上なら2回必要になる。
サイクルに出てくる数たちを列としてみると、全体をreverseするのと1狭い区間をreverseするのをやると1shiftが作れます。
例えば234561→[165432]→1[23456]という感じです。reverseは複数のswapを並列に行うだけで実現できますね。
E (原案:japlj)
まずはクラスタリングしましょう。
クラスタリングは超高精度でできます。
各人に対して、その人と同じ選択肢が選ばれている回数を求めて、その順でソートして10,10,10と区切ってやればいいです。
あとは各問題について最尤値推定をすればいいです。(2/3の人で多数決をするだけでも通ります)
F (原案:snuke)
見掛け倒しです。
実は操作は対称性が高く、K次回文はreverseしてもK次回文になります。
mod2で答えればいいので、もともと回文であるようなK次回文の個数さえ求めれば良いことになります。
K=0のときは回文の個数を求めればいいです。
K>=2のときは追加、置換*K-2、削除とすればもとの文字列に戻すことが出来るので、これも回文の個数を求めればいいだけです。
K=1のときは「もとが回文」かつ「1長いものまたは1短いものも回文」でなければならず、どの文字とどの文字が同じでなければならないかを考えると、すべての文字が同じ文字列しか条件を満たさないということがわかります。
G (原案:snuke)
最大流に落としましょう。
そのグラフは平面グラフになっているので、最小カットを最短路で求めることができます。
有向グラフの場合は逆辺としてコスト0の辺を張らないといけない点に注意。
H (原案:snuke)
二分探索した後、結構シンプルな貪欲法で解けます。証明が面白いです。
証明は後で。
I (原案:snuke)
いろんな塗り分け方をして偶奇性を見ると各テトロミノ特有の性質というのが検出できます。
後で。
J (原案:japlj)
条件を満たすように数列を作ってくださいとしか解説のしようがあまりない気がする。
(1)はなんと、全部満たせます!
(2)は後のことをあまり考えずに貪欲に置ける数の中で最も大きいものを置いていくだけでいいです。変態的なパズルです。

open all / close all


generated by indenter

ACC2016 24日目「色塗り2」解説

Advent Calendar Contest 2016 24日目の問題「色塗り2」を出題させていただいたので、その解説
問題はこちら。

解説
まずは「葉が b 個根に付いた根付き木の頂点を c 色で塗り分ける場合の数」を考えてみます。
コラム
根付き木の同型性に関して扱うときの常套テクとして、各子に何らかの順序をつけてソートすることによって標準化をします。
つまり、「葉に塗る色の番号は左から見たときに昇順になっていなければならない」というルールを付けて数え上げることにすると良いです。
そうすることで、重複無く数え上げることができます。
立式をしてみましょう。
よく見るとまんま重複組み合わせですね。
 {}_c H_b*c = {}_{c+b-1} C_b*c
 _c H_b は葉の塗り分け方、 *c は根の塗り方。
元の問題に戻って考えてみます。
「葉が b 個根に付いた根付き木の頂点を c 色で塗り分ける場合の数」を d とします。
すると、元の問題の場合の数は「葉が a 個根に付いた根付き木の葉を d 色、根を c 色で塗り分ける場合の数」と同じです。
式にするとこうなります。
 {}_d H_a*c = {}_{d+a-1} C_a*c
dを展開した式はこうなります。
 {}_{({}_c H_b*c)} H_a*c
式の計算方法を考えます。
 {}_{({}_c H_b*c)} H_a*c の偶奇を求めたいのですが、何も考えずにmod 2で計算してしまうとおかしなことになります。
例えば、 {}_5 C_4 \equiv 0 \ne 1 \equiv {}_1 C_0 という具合です。
さてここで、 {}_x C_y\ mod\ 2 の性質について思いを馳せます。
結論から言うと、 x\&y = y なら 0、そうでないなら 1 です。
パスカルの三角形の偶奇を眺めてみたりすると分かるかもしれません。
ここで重要なのが、y を超えるような2冪数を T としたとき、 x\&y = (x\%T)\&y であることです。
つまり、x の正確な値が分かっていなくても、 x\ mod\ T の値さえ分かっていれば  {}_x C_y\ mod\ 2 が計算できます。
 a \lt 2^{20} なので、 {}_c H_b*c\ mod\ 2^{20} が求められれば良いことになりました。
あとは剰余演算に慣れた人ならそれほど難しくありません。
剰余演算パート
 {}_c H_b*c\ mod\ 2^{20} の求め方を考えます。
 {}_c H_b = {}_(c+b-1) C_b = (c+b-1)!/b!/(c-1)! なので、階乗どうしの割り算が出来れば良いです。
階乗は前計算しておけばいいのですが、そのまま計算すると割り算で詰むので、工夫が必要です。
値を「奇数*(2冪)」という形に分解し、「奇数」と「指数の肩」の組  (O,E) を計算しておきます。
すると  x/y = O_x/O_y*2^{E_x-E_y} という形で計算ができます。
残る問題は奇数での割り算ですが、一般にmodの値と互いに素な数には逆元が存在するため割り算が可能です。
xのmod mでの逆元は  x^{\phi(m)-1} と計算できます。
 \phi(m)オイラー関数(mと互いに素なm以下の整数の個数)で、 \phi(2^{20}) = 2^{19} です。
詳しくは蟻本を読むと良いでしょう。更に詳しくは群論とかの本を読むといいかも。
おまけ
元ネタ
KUPC2016のI問題「色塗り」で、本番でそのままmodで計算していってもいいのか考えて、mod 2だとダメだよなぁと思ったのがこの問題を作ったきっかけです。
mod 10億7だとコンビネーションの分母がmodと互いに素になるのでOKっぽい。
発展課題
この問題ではmod 2までで諦めてしまいましたが、 {}_x C_y\ mod\ 2^z を高速で計算できたりしませんかね?→sigma氏からnCm mod 素べきの計算法の論文を教えてもらいました

open all / close all

generated by indenter