Editorial for 第3回卬高杯 D問題 - Soda's Social Studies


Remember to use this editorial only when stuck, and not to copy-paste code from it. Please be respectful to the problem author and editorialist.
Submitting an official solution before solving the problem yourself is a bannable offence.

Authors: admin, RenCoda3

ヒント

ヒント1 \(3\)つの物事を覚える場合について考えましょう。
例えば、物事\(1\)と物事\(2\)をリンクさせ、その後物事\(2\)と物事\(3\)をリンクすると、物事\(1\)から物事\(3\)を、物事\(2\)を通して連想する事ができるようになります。逆も同様です。
ヒント2 \(N\)個の事柄を繋げるためには、最も効率よく覚える場合は何回覚えるだけで済むでしょうか。
ヒント3 (ヒント2の答え) 最も効率よく覚える時、\(N\)個の事柄をリンクさせるためには、\(N-1\)回覚えるだけで十分です(最悪、順番に繋げていけばいいだけの話)。
そのため、そーだくんの記憶は、全体または一部が環状に繋がるようなことはなく、必ず枝状の構造になります。
(実際、対策は多いに越したことはありませんが…ここでは、そーだくんは短い時間で覚えるようにしなければなりません)
ヒント4 \(T_{(i,j)} \quad (1 < i ≤ j ≤ N)\) が一番小さい所から順に探すようにすれば、何か見えてくるかも。

概説

この問題は、\(1 ≤ i < j ≤ N\)を満たす全ての整数の組\((i,j)\)について、頂点\(i\)と頂点\(j\)に重みが\(T_{(i,j)}\)である辺を張った完全グラフの最小全域木の重みが答えになります。 最小全域木とは、「辺の重みの和が最小になり、かつ全ての頂点が連結している、閉路(環状のような構造)がないグラフ」のことです。最小全域木の重みは、これを構成する辺の重みの総和になります。

最小全域木を求めるためには、クラスカル法やプリム法などが用いられます。 例えば、クラスカル法は貪欲法の一種であり、「辺を重みが小さいものから、閉路を作らないように選ぶ」という方法です。問題文に即して言えば、「早く覚えられるものから、記憶の一部または全部が環状にならないように覚えていく」といった覚え方です。

辺の数を\(V\)本とすると、クラスカル法の時間計算量は\(O(VlogV)\)です。辺の数は\(\frac{N\times(N-1)}{2}\)本で、この制約下\((1 < N ≤ 300)\)では辺の数は最大\(44850\)本ですから、実行制限時間内に解く事ができます。

どの頂点と繋がっているかを確かめるにはUnion-Findなどを利用する事がありますが、下に続く実行例では再帰関数を用いてそれらしくしています。


実装例

クラスカル法を利用した実装例(C++)
#include <bits/stdc++.h>
using namespace std;

vector<int> par={}; //後述。

int root(int a){
  if(par[a]==a) return a;
  else return root(par[a]);
}
/*
    rootという再帰関数を設定した。連鎖的に思い出せる仕組みを、この4行で構築している。
*/

int main() {
  int n; cin >> n;
  vector<tuple<int,int,int>> v;
  for(int i=0; i<n; i++){
    int a=i;
    for(int j=i+1; j<n; j++){
      int b=j;
      int c; cin >> c;
      v.push_back(make_tuple(c,a,b));
    }
    par.push_back(i);
    /*
    parには、思い出せる単語の中で一番番号が小さいものを収納する。
    0-index化も同時に行なっている。
    例えば単語5から単語1を思い出せる場合には、par[5-1]=1-1、つまりpar[4]=0が収納されている。
    ここでは全て単語そのものしか思い出せない状態という意味で、par[i]=iを代入している。
    */
  }
  sort(v.begin(),v.end());

  int64_t ans=0;
  for(int i=0; i<v.size(); i++){
    tuple<int,int,int> t=v[i];
    int a=get<1>(t); int b=get<2>(t); int c=get<0>(t);
    if(root(a)==root(b)){ // 同じものを思い出せる場合は繋げても無駄なので、何もしない。
      continue;
    }
    else{
      par[root(b)]=root(a); // 断片的に繋がった記憶2つを合体している。
      ans+=c;
    }
  }
  cout << ans << endl;
}

補足

  • クラスカル法、BFS(幅優先探索)やDFS(深さ優先探索)の上位互換とも言われることがあるみたいです。前回もBFSを使って解かせたところがあるので、何とか他のアルゴリズムも学びたいところ。
  • ちなみに、そーだくんが知っている単語が全て穴埋めで出題されるなど、問題文に単語が見つからなかったら、0点です。可哀想に……


Comments

There are no comments at the moment.