ABCに初出場してレートがついた/タバコの力学

Atcoder Beginner Contest。略してABC第56回に参加しました。

予想通りボロボロでしたが、初めてレートがついたので嬉しかったです。順位は256位でついたレートは46です。灰色なので競プロ界では灰色コーダーと呼びます。Twitterでフォローしてる競プロ界の上にいる方々は、どうやら何か大きな大会があるらしく合宿の様子がTLに流れてきます。いいですね。僕は参加資格ありませんが、いつか問題を解けるようになりたいです。

ABCではC問題が解けず、時間の途中で諦めてしまいました。終わってから解説を確認したところ、こんな方法で解けるのかと目からウロコでした。他にもC問題に挑戦したのですが、与えられた情報を考察して、プログラムが扱いやすいようにする問題が多いなという印象です。

他にもこんな解き方があるのかと関心したのはABC第52回のFactors of Factorialです。与えられた数の階乗の素因数分解をして、約数の数+1を掛け合わせ、その数を1000000007で割った余りが答えなのですが、複雑なデータ構造を使わず配列とループだけで解く方法に脱帽しました。今週はC問題に集中したいです。目標としては今週から1ヶ月以内にABCでC問題を一つ解くことです。

f:id:ilovehugeworld:20170320110934p:plain

そういえば、今週は春休みだったのですが普段通り仕事に行って、競プロしてました。あとは、友達と呑んでタバコを吸って、タバコがコミュニケーションにもたらす効果を実感しました。面白い発見だったので詳しく書きます。

人は不安なときに唇を触るのですが、これは赤ちゃんのときにミルクを吸ってたときの安心感を思い出すためと言われてます。そして、脳は安心したいから唇を触ったのか、唇を触ったから安心感を得られたのかの区別がつきません。つまり、タバコを咥えたことで安心感を覚えるのですが、脳はそれを錯覚し目の前の人が安心感を与えたと認識します。また、タバコを吸う場合は今までいた人達から離れ、場を共有します。これが一種の秘密の共有のような錯覚を覚えさせ、ポロッと大事なことを伝えてしまいます。タバコでコミュニケーションが生まれるというのは、こういう理屈なのです。実際、僕も言うつもりのないことを、ポロッと伝えてしまい、あ、これはすごいと思いました。

ネットで得た知識を元にした考察なので、何か違うよて場合はコメントで教えてくださると嬉しいです。意味わからんて方は、僕の説明力不足です。


と、こんな感じの1週間でした。水曜日にOperating Systemのクラスがあるので、ヒンディ語と英語が混じったYouTubeの解説動画を見て勉強しようと思います。今週解いた問題は、UVa 247UVa 11838UVa 11709UVa 11504でした。今週はC問題にフォーカスします。

階乗の素因数分解の仕方

ABC第52回のFactors of Factorialという問題を解いていたのですが、階乗の素因数分解をする必要がありました。やり方をしらなかったので、その方法のメモです。

この問題の答えを最初intで保持していたためinputが1000のときに正しい答えを得られず1時間ほど悩みました。10*9+7が出てきたときは必ずlongを使うと決めてるといいかもしれません。

import java.util.*;

/* 階乗の素因数分解の仕方。参考: https://youtu.be/28IanD3lXGg */

public class Playground2 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        
        int n = in.nextInt();
    
        int[] numExp = new int[n + 1];
        for (int i = 2; i <= n; i++) {
            int a = i;
            while (a > 1) {
                for (int j = 2; j <= i; j++) {
                    if (a % j == 0) {
                        numExp[j]++;
                        a /= j;
                        break;
                    }
                }
            }
        }
        
        for (int i = 2; i <= n; i++) {
            if (numExp[i] != 0) {
                System.out.printf("%d: %d\n", i, numExp[i]);
            }
        }
    }
    
}

何かの参考になるかもしれないので、一応自分が最初に書いたコードを置いときます。

import java.util.*;

/* 階乗の素因数分解の仕方。参考: https://youtu.be/28IanD3lXGg */

public class Playground2 {

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        
        int n = in.nextInt();
        ArrayList<Integer> primes = new ArrayList<Integer>();
        fillPrimes(primes, n);
        printPrimeFactor(primes, n);
    }
    
    // Print prime factor of n factorial
    public static void printPrimeFactor(ArrayList<Integer> primes, int n) {
        for (int i = 0; i < primes.size(); i++) {
            int p = primes.get(i);
            int nn = p;
            int numExp = 0;
            while (nn <= n) {
                numExp += n / nn;
                nn *= p;
            }
            System.out.printf("%d: %d\n", p, numExp);
        }
    }
    
    // Put prime number up to n (inclusive)
    public static void fillPrimes(ArrayList<Integer> primes, int n) {
        int[] prime = new int[n + 1];
        Arrays.fill(prime, 1);
        
        primes.add(2);
        
        for (int i = 3; i <= n; i += 2) {
            if (prime[i] == 1) {
                if (isPrime(i)) {
                    primes.add(i);
                    for (int j = i * 2; j <= n; j += i) {
                        prime[j] = 0;
                    }
                }
            }
        }
    }
    
    public static boolean isPrime(int n) {
        for (int i = 2; i <= Math.sqrt(n); i++) {
            if (n % i == 0) {
                return false;
            }
        }
        return true;
    }
}

強連結成分分解においてどうしてスタックにあることを確かめる必要があるのか

強連結成分分解(SCC)を発見するTarjanアルゴリズムを学んでいたのですが、どうしてスタックにあるかどうかを確認する必要があるかがわからなかったのでその答えです。

答えを確かめるためにUVa 247を使いました。僕が解くために書いたソースコードは以下のとおりです。関節点で書いた記事と同じで、訪れたノードの順番をdfs_numに。子ノードから訪れることができる最小の親ノードの順番をdfs_lowに保存しています。

public static int[] dfs_num;
public static int[] dfs_low;
public static Stack<Integer> stack;
public static int[] inStack;
public static int dfs_counter;

public static void main(String[] args) {
  
  // Graphを隣接リストとして取得。変数名はadjList

  for (int i = 0; i < adjList.length; i++) {
    if (dfs_num[i] == 0) {
      solve(i);
    }
  }
}

public static void solve(int curr) {
  dfs_num[curr] = dfs_counter;
  dfs_low[curr] = dfs_counter;
  dfs_counter++;

  stack.push(curr);
  inStack[curr] = 1;
  ArrayList<Integer> edge = adjList[curr];

  for (int i = 0; i < edge.size(); i++) {
    int next = edge.get(i);
  
    if (dfs_num[next] == 0) {
      solve(next);
    }
    if (inStack[next] == 1) { // どうしてここのチェックが必要なのかがわからない。
      dfs_low[curr] = Math.min(dfs_low[curr], dfs_low[next]);
    }
  }

  if (dfs_num[curr] == dfs_low[curr]) {
    int pop = stack.pop();
    System.out.printf("SCC: (");
    while (pop != curr) {
      System.out.printf("%d ", pop);
      inStack[pop] = 0;
      pop = stack.pop();
    } 
    inStack[pop] = 0;
    System.out.printf("%d)\n", pop);
  }
}

どうして、スタックにあることを確かめる必要があるかはこういうテストケースを考えると理解できます。

f:id:ilovehugeworld:20170315101455p:plain

solveを呼ぶforループは0から始まるので、最初はノード0をチェックします。ノード0からいけるノードはないので再帰的にsolveを呼ぶことなく終了し、SCC (0)をプリントします。次にsolve(1)が呼ばれます。今回はエッジがあるのでforループに入り、問題のif文に来ます。もし、ここでif (inStack[next] == 1)がなければそのままノード0のdfs_low[0]の1を取得しdfs_low[1]に入れてしまいます。

そうすると、forループ後のdfs_num[curr] == dfs_low[curr]がtrueではなくなるため、SCC (1)をプリントしません。つもりこのif文は、今調べてるノードのdfs_low値を、すでに強連結されてるノードのdfs_lowで上書きしないためのif文だったのです。

逆にいうとnextが今調べてるノードと強連結する可能性がある時だけdfs_low[curr]をアップデートするという感じでしょうか。どうもわかりやすくできないかなと思ったのですが、何も思いつきませんでした。自分がきちんと実装できてるかを確かめるためにUVa 247を解いてみるといいかもしれません。