CodinGame - Ghost in the Cell 参加日記

CodinGameのコンテストに初参加してみた。ゲームAIを作ってその強さを競うものなのだが、面倒な環境構築が不要でなかなか楽しかった。対戦動画も観ていて面白い。

https://www.codingame.com/replay/194505045
黄色が私。最終的にCyborgの数が多いほうが勝ち

ランキングはリーグ制。最初はウッドリーグ3部からスタートし、リーグボスを倒すごとにウッド2→ウッド1→ブロンズ→シルバー→ゴールドとランクアップする。また、途中でルールが追加されることもある。

ゲームルール概略

  • ターン制
  • マップ上にいくつかの工場がある
  • 工場間は長さの違う道で繋がれている
  • 工場は味方・敵・中立のいずれか
  • 工場は1ターンごと0~3体のCyborgを自動生産する
  • 1ターンごと、味方・敵同時に行動を起こす
  • 自工場からほかの工場にCyborg部隊を送ることができる
  • 敵または中立の工場にCyborg部隊が到着したら戦いが起こる
  • 戦いはCyborg数の多いほう勝利し、数の差分だけ生き残る
  • 戦いに勝ったら工場を占領できる
  • 最終的に相手を全滅させるか、400ターン後にcyborgの多いほうが勝ち
  • 1ターンに複数の部隊を送ることができる*
  • Cyborgを10消費して、工場のCyborg生産数を増やすことができる(3まで)*
  • 2回まで爆弾を送ることができる*
  • 爆弾を被弾すると、工場のCyborg数が半分、または10まで減る(小さいほう)*

(*はリーグが上がるごとに追加されたルール)

実装
1ターンの制限時間が50msしかないので、深く手を読む方針はとらなかった。基本的に、(1)危ない自工場があったら助ける(2)生産力のある中立工場があったら占領しようとする(3)敵に爆弾を送ってみる(4)敵工場を占領しようとする(4)遊んでる工場があったら前線に兵を送る、の優先順位で手の候補をいくつか作成し、そこから21手くらい先までシミュレートして局面評価がベストのものを採用した。

反省
序盤にいろいろ考えすぎて実装が遅れたのが良くなかった。Topcoderのマラソンマッチと違い途中でルールが変わるし、またリーグが落ちることは(たぶん)ない仕組みなので、軽いコードをどんどん書いてSubmitしていったほうがよさそうだ。シミュレーション中の手や評価関数がまったく詰められなかったのも反省点。

結果はぎりぎりシルバーリーグの103位/311人だった。次回はシルバー上位を目標にしよう。

HackerRank Ad Infinitum16 参加日記

始めて参加した。数学関連の問題がでるコンテストらしい。
46位/673人だけど初回参加者用の区分だっただからレベルが低いのかも。

ad Infinitum: 無限に、永久に(ラテン語)


Leonardo's Prime Factors

q個の整数が与えられる。それぞれの整数(nとする)について、集合[1,n]内で、一意な素因数の数が最も多いものを算出し、その数を出力せよ。
1 <= q <= 10^5
1 <= n <= 10^18

素数を小さい順に、nを超えるまでかけていけばよい。素数は50くらいまでしか使わないので、最初に列挙しておくと効率が良い。

var q = Int32.Parse(Console.ReadLine());
while (q > 0)
{
    var n = Int64.Parse(Console.ReadLine());
    var ret = 0;
    BigInteger x = 1;

    foreach (var p in primes) //primesは素数のリスト(昇順)
    {
        x *= p;
        if (x > n) break;
        ret++;
    }
    Console.WriteLine(ret);
    q--;
}


Russian Peasant Exponentiation

q個のクエリが与えられる。それぞれのクエリでは、整数k,m,a,bが与えられる。(a+bi)^kを求めよ(iは虚数単位)。解は、実部と虚部をそれぞれmでModしたものを出力せよ。
大きなべき乗を求める方法は次を参考にせよ
https://www.hackerrank.com/external_redirect?to=http://lafstern.org/matt/col3.pdf
1 <= q <= 10^5
0 <= k <= 10^18
2 <= m <= 10^9
0 <= a, b <= m

「ロシア農民のアルゴリズム」を複素数に応用しろというもの。複素数クラスを作成し、乗算とModを実装すれば、実数と同じコードになる。複素数クラスはNumericsのものだとオーバーフローの危険性がありそうだ。

static void Main(string[] args)
{
    Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false });

    var q = Int32.Parse(Console.ReadLine());
    while (q > 0)
    {
        //a,b,k,m -> (a + bi)^k mod m
        var str = Console.ReadLine().Split();
        var a = Int64.Parse(str[0]);
        var b = Int64.Parse(str[1]);
        var k = Int64.Parse(str[2]);
        var m = Int64.Parse(str[3]);

        var comp = new MyComplex(a, b);
        var modPow = ModPow(comp, k, m);

        //mod前のmの足し忘れ注意!
        Console.WriteLine(string.Format("{0} {1}", (modPow.Real + m) % m, (modPow.Imaginary + m) % m));
        q--;
    }
    Console.Out.Flush();
}

//ロシア農民のアルゴリズム
static MyComplex ModPow(MyComplex x, Int64 n, Int64 mod)
{
    while ((n & 1) == 0)
    {
        x = (x * x) % mod;
        n >>= 1;
    }
    var ret = x;
    n >>= 1;
    while (n > 0)
    {
        x = (x * x) % mod;
        if ((n & 1) != 0)
            ret = (ret * x) % mod;
        n >>= 1;
    }

    return ret % mod;
}

class MyComplex
{
    public Int64 Real;
    public Int64 Imaginary;

    public MyComplex(Int64 real, Int64 img)
    {
        Real = real;
        Imaginary = img;
    }

    public static MyComplex operator *(MyComplex c1, MyComplex c2)
    {
        return new MyComplex(c1.Real * c2.Real - c1.Imaginary * c2.Imaginary, c1.Real * c2.Imaginary + c1.Imaginary * c2.Real);
    }

    public static MyComplex operator %(MyComplex c1, Int64 mod)
    {
        return new MyComplex((c1.Real) % mod, (c1.Imaginary) % mod);
    }
}


Xrange's Pancakes

正n角形のパンケーキが与えられる。このパンケーキは、①中心と各頂点を通るもの②中心と各頂点の中間点を通るもの、の2種類の軸が存在する(合計n本)。軸は0~n-1の番号が時計回りに連続でついている。
このとき、n個のクエリが与えられる。クエリの種類は2つ:
タイプ1:軸kで裏返す
タイプ2:パンケーキを、(360*k) / n 度だけ時計回りに回転させる
すべてのクエリが終了したときのパンケーキを、タイプ1またはタイプ2を1回だけ適用させて、初期位置に戻したい。このクエリを答えよ。
3 <= n <= 10^6
1 <= m <= 10^6
0 <= k <= n

ややこしく考えると嵌りそうだが、実際は初期に軸0にあった頂点の位置と、裏返されているか否かだけ管理すればよい。

var str = Console.ReadLine().Split();
var n = Int32.Parse(str[0]);
var m = Int32.Parse(str[1]);

var x = 2 * n; //位置(頂点+頂点間)
var pos = 0;   //初期に軸0にあった頂点の位置
var isFlipped = false;

while (m > 0)
{
    str = Console.ReadLine().Split();
    var k = Int32.Parse(str[1]);
    switch (str[0])
    {
        case "1":   //rotate
            pos = (pos + k * 2) % x;
            break;
        default:    //flip
            pos = (pos + ((k + x - pos) % x) * 2) % x;
            isFlipped = !isFlipped;
            break;
    }
    m--;
}

if (!isFlipped)
{
    Console.WriteLine("1 " + ((x - pos) / 2).ToString());
}
else
{
    Console.WriteLine("2 " + (pos / 2).ToString());
}


Solve Equations

方程式 { ax + by = c }がq個与えられたとき、それぞれ次を満たす最も原点に近い点を求めよ。
・xとyは整数
・xは0より大きい
複数の解があるときは、xのより小さいものを答えよ。
1 <= q <= 10^5
1 <= a <= 10^8
1 <= b <= 10^8
1 <= c <= 10^8
a, b, cは整数

整数解が存在するといことは、cはgcd(a,b)で割り切れる。そして、拡張ユークリッドアルゴリズムを使って一組の(x, y)が計算できたとき、すべての組は次の形で表せる。ベズーの等式 - Wikipedia
 { \left(x+k{\frac  {b}{\gcd(a,b)}},\ y-k{\frac  {a}{\gcd(a,b)}}\right) }
ここで、 { c_1 = \frac {b}{\gcd(a, b)} } { c_2 = - \frac {a}{\gcd(a, b)} }とすると
 { x_2 = x_1 + c_1 \cdot k }
 { y_2 = y_1 + c_2 \cdot k }
と表すことができる。
 { (x_2)^2 + (y_2)^2 } を最小化するのが目的なので、上式の展開
 { (x_1 + c_1\cdot k)^2 + (y_1 + c_2 \cdot k)^2 = t^2 \cdot ((c_1)^2 + (c_2)^2) + t \cdot (2 \cdot x_1 \cdot c_1 + 2 \cdot y_1 \cdot c_2) + ( (x_1)^2 + (y_1)^2 ) }
を最小化すればよい。
 { \alpha = (c_1)^2 + (c_2)^2 }
 { \beta = 2 \cdot x_1 \cdot c_1 + 2 \cdot y_1 \cdot c_2 }
 { \gamma = (x_1)^2 + (y_1)^2 }
とおくと、
 { \alpha \cdot t^2 + \beta \cdot t + \gamma }
になる。よって答えは以下のようになる。(実装は省略)
1. { - \frac{\beta}{ 2 \cdot \alpha } } が負のとき
{ t=0 } の場合
2. { - \frac{\beta}{ 2 \cdot \alpha } } が正のとき
{ t= -\frac{\beta}{ 2 \cdot \alpha } } または{ t= -\frac{\beta}{ 2 \cdot \alpha } + 1 } の場合


Hyperspace Travel

m次元の超空間にn人が存在している。この人達は、次元のグリッド線上だけを動くことができる。全員の総移動距離が最小になるよう、空間のある地点に集まりたい。この地点を求めよ。
各人の初期位置は、次元0、次元1...の順にx0,x1...の形で与えられる。
1 <= n <= 10^4
1 <= m <= 10^2

  • 10^9 <= xi <= 10^9 (整数のみ)

集合地点の位置は整数

グリッド線上のみ動くことができる(次元間をナナメに移動することはできない)ということは、各次元内で移動が完結しているということになる。よって、次元ごとに集合地点を求めればよい。
ある次元について、移動距離が最小になるのは、真ん中にいる人に集まる場合なので、ソートしてIndexが中点のものが答えになる。

static void Main(string[] args)
{
    var str = Console.ReadLine().Split();
    var n = Int64.Parse(str[0]);
    var m = Int64.Parse(str[1]);

    var friends = new List<List<Int64>>();
    for (int i = 0; i < n; i++)
    {
        friends.Add(Console.ReadLine().Split().Select(v => Int64.Parse(v)).ToList());
    }

    for (int i = 0; i < m; i++)
    {
        Console.Write(Get(i, friends));
        if (i != m - 1) Console.Write(" ");
    }
    Console.WriteLine();
}

static Int64 Get(int dimension, List<List<Int64>> friends)
{
    var values = new List<Int64>();
    foreach (var friend in friends)
    {
        values.Add(friend[dimension]);
    }
    values.Sort();

    return values[(values.Count() - 1) / 2];
}

イントロソートを安定ソートとして習作(C#)

C#で安定ソートを行うときはLinqのOrderBy()が一般的だが、OrderByはクイックソートなのでワースト計算量がO(N^2)になってしまう。ここでは、これを回避したソートを習作してみる。

ちなみにC++のstable_sort()だと、安定マージソートをつかってこの問題を回避している。配列を再帰的に分割していき、長さが閾値(15)未満になったら挿入ソートに切り替えるようだ。
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01347.html#l03409

これをそのまま移植してもOKなのだが、それでは面白くないので、ここではArray.Sort()のイントロソートを強引に安定化することにする。イントロソートは最初はクイックソートを行い、再帰のレベルがある値を超えたらヒープソートに切り替えるといもので、高速なクイックソートの利点を利用しつつ最悪ケースを回避するアルゴリズムだ。基本的にはこの2つのソートを安定版に作り替えればよい。

以下のMS実装を参考にした。

イントロソート(Array.Sort())
https://referencesource.microsoft.com/#mscorlib/system/array.cs,2a2126edd9ca7eb4
クイックソート(Linq)
https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1395017e067e5a34

static public void StableSort<T>(List<T> list) where T : IComparable
{
    int[] map = new int[list.Count()];
    for (int i = 0; i < list.Count(); i++) map[i] = i;
    StableSort(list, map, 0, list.Count() - 1, (int)Math.Floor(Math.Log(list.Count(), 2)) * 2);

    var tmp = list.ToArray();
    for (int i = 0; i < list.Count(); i++)
    {
        list[i] = tmp[map[i]];
    }
}

static void StableSort<T>(List<T> list, int[] map, int left, int right, int depthLimit) where T : IComparable
{
    if (left >= right) return;

    //stable heap sort
    if (depthLimit == 0)
    {
        StableSort_HeapSort(list, map, left, right);
        return;
    }

    //stable quick sort
    do
    {
        int i = left;
        int j = right;
        int x = map[i + ((j - i) >> 1)];
        do
        {
            while (i < list.Count() && CompareKeys(list, x, map[i]) > 0) i++;
            while (j >= 0 && CompareKeys(list, x, map[j]) < 0) j--;
            if (i > j) break;
            if (i < j)
            {
                int temp = map[i];
                map[i] = map[j];
                map[j] = temp;
            }
            i++;
            j--;
        } while (i <= j);
        if (j - left <= right - i)
        {
            if (left < j) StableSort(list, map, left, j, depthLimit - 1);
            left = i;
        }
        else
        {
            if (i < right) StableSort(list, map, i, right, depthLimit - 1);
            right = j;
        }
    } while (left < right);
}

static void StableSort_HeapSort<T>(List<T> list, int[] map, int lo, int hi) where T : IComparable
{
    int n = hi - lo + 1;
    for (int i = n / 2; i >= 1; i = i - 1)
    {
        StableSort_HeapSort_DownHeap(list, map, i, n, lo);
    }
    for (int i = n; i > 1; i = i - 1)
    {
        Swap(map, lo, lo + i - 1);
        StableSort_HeapSort_DownHeap(list, map, 1, i - 1, lo);
    }
}

static void StableSort_HeapSort_DownHeap<T>(List<T> list, int[] map, int i, int n, int lo) where T : IComparable
{
    int d = map[lo + i - 1];
    int child;
    while (i <= n / 2)
    {
        child = 2 * i;
        if (child < n && CompareKeys(list, map[lo + child - 1], map[lo + child]) < 0)
        {
            child++;
        }
        if (!(CompareKeys(list, d, map[lo + child - 1]) < 0))
            break;
        map[lo + i - 1] = map[lo + child - 1];
        i = child;
    }
    map[lo + i - 1] = d;
}

static void Swap(int[] map, int i, int j)
{
    var tmp = map[i];
    map[i] = map[j];
    map[j] = tmp;
}

static int CompareKeys<T>(List<T> list, int index1, int index2) where T : IComparable
{
    var c = list[index1].CompareTo(list[index2]);
    if (c == 0)
    {
        return index1 - index2;
    }
    return c;
}

当然、ほとんどの場合でOrderByのほうが少し速い。ワーストケースではこちらのほうが大幅に良いパフォーマンスが出るはず・・・だがうまくテストケースを作れなかった。

平衡二分探索木を使ったsetとmultisetの実装(C#)

C++のsetとmultisetに相当するコレクションをC#で実装してみる。

set
順序付けされたデータを重複を排除して保持するもの。C#のSortedSetとほぼ同じだが、lower_bound()とupper_bound()が使える。データの追加・削除・検索いずれもO(logN)。

multiset
順序付けされたデータを重複を許容しながら保持するもの。C#に類似のものはない。setと同じく、lower_bound()とupper_bound()がある。データの追加・削除・検索いずれもO(logN)。

lower_bound()は該当値以上になる最小Indexを、upper_bound()は該当値より大きくなる最小Indexを返す関数で、いずれもO(logN)になる。

どちらも平衡二分探索木で実現できる。平衡二分探索木は二分探索木を、木構造がなるべく偏らないように工夫したものである(偏ると効率が悪くなる=O(N)に近くなる)。ここではRandomized Binary Search Tree(RBST)と呼ばれる、値を追加するときに、なるべく木の高さの偏りがなくなるよう、追加候補の位置に重みづけをしてランダム選択する方法で実装してみる。

詳細はこちらが参考になった
プログラミングコンテストでのデータ構造 2 ~平衡二分探索木編~

/// <summary>
/// Self-Balancing Binary Search Tree
/// (using Randamized BST)
/// </summary>
public class SB_BinarySearchTree<T> where T : IComparable
{
    public class Node
    {
        public T Value;
        public Node LChild;
        public Node RChild;
        public int Count;     //size of the sub tree

        public Node(T v)
        {
            Value = v;
            Count = 1;
        }
    }

    static Random _rnd = new Random();

    public static int Count(Node t)
    {
        return t == null ? 0 : t.Count;
    }

    static Node Update(Node t)
    {
        t.Count = Count(t.LChild) + Count(t.RChild) + 1;
        return t;
    }

    public static Node Merge(Node l, Node r)
    {
        if (l == null || r == null) return l == null ? r : l;

        if ((double)Count(l) / (double)(Count(l) + Count(r)) > _rnd.NextDouble())
        {
            l.RChild = Merge(l.RChild, r);
            return Update(l);
        }
        else
        {
            r.LChild = Merge(l, r.LChild);
            return Update(r);
        }
    }

    /// <summary>
    /// split as [0, k), [k, n)
    /// </summary>
    public static Tuple<Node, Node> Split(Node t, int k)
    {
        if (t == null) return new Tuple<Node, Node>(null, null);
        if (k <= Count(t.LChild))
        {
            var s = Split(t.LChild, k);
            t.LChild = s.Item2;
            return new Tuple<Node, Node>(s.Item1, Update(t));
        }
        else
        {
            var s = Split(t.RChild, k - Count(t.LChild) - 1);
            t.RChild = s.Item1;
            return new Tuple<Node, Node>(Update(t), s.Item2);
        }
    }

    public static Node Remove(Node t, T v)
    {
        if (Find(t, v) == null) return t;
        return RemoveAt(t, LowerBound(t, v));
    }

    public static Node RemoveAt(Node t, int k)
    {
        var s = Split(t, k);
        var s2 = Split(s.Item2, 1);
        return Merge(s.Item1, s2.Item2);
    }

    public static bool Contains(Node t, T v)
    {
        return Find(t, v) != null;
    }

    public static Node Find(Node t, T v)
    {
        while (t != null)
        {
            var cmp = t.Value.CompareTo(v);
            if (cmp > 0) t = t.LChild;
            else if (cmp < 0) t = t.RChild;
            else break;
        }
        return t;
    }

    public static Node FindByIndex(Node t, int idx)
    {
        if (t == null) return null;

        var currentIdx = Count(t) - Count(t.RChild) - 1;
        while (t != null)
        {
            if (currentIdx == idx) return t;
            if (currentIdx > idx)
            {
                t = t.LChild;
                currentIdx -= (Count(t == null ? null : t.RChild) + 1);
            }
            else
            {
                t = t.RChild;
                currentIdx += (Count(t == null ? null : t.LChild) + 1);
            }
        }

        return null;
    }

    public static int UpperBound(Node t, T v)
    {
        var torg = t;
        if (t == null) return -1;

        var ret = Int32.MaxValue;
        var idx = Count(t) - Count(t.RChild) - 1;
        while (t != null)
        {
            var cmp = t.Value.CompareTo(v);
                
            if (cmp > 0)
            {
                ret = Math.Min(ret, idx);
                t = t.LChild;
                idx -= (Count(t == null ? null : t.RChild) + 1);
            }
            else if (cmp <= 0)
            {
                t = t.RChild;
                idx += (Count(t == null ? null : t.LChild) + 1);
            }
        }
        return ret == Int32.MaxValue ? Count(torg) : ret;
    }

    public static int LowerBound(Node t, T v)
    {
        var torg = t;
        if (t == null) return -1;

        var idx = Count(t) - Count(t.RChild) - 1;
        var ret = Int32.MaxValue;
        while (t != null)
        {
            var cmp = t.Value.CompareTo(v);
            if (cmp >= 0)
            {
                if (cmp == 0) ret = Math.Min(ret, idx);
                t = t.LChild;
                if (t == null) ret = Math.Min(ret, idx);
                idx -= t == null ? 0 : (Count(t.RChild) + 1);
            }
            else if (cmp < 0)
            {
                t = t.RChild;
                idx += (Count(t == null ? null : t.LChild) + 1);
                if (t == null) return idx;
            }
        }
        return ret == Int32.MaxValue ? Count(torg) : ret;
    }

    public static Node Insert(Node t, T v)
    {
        var ub = LowerBound(t, v);
        return InsertByIdx(t, ub, v);
    }

    static Node InsertByIdx(Node t, int k, T v)
    {
        var s = Split(t, k);
        return Merge(Merge(s.Item1, new Node(v)), s.Item2);
    }

    public static IEnumerable<T> Enumerate(Node t)
    {
        var ret = new List<T>();
        Enumerate(t, ret);
        return ret;
    }

    static void Enumerate(Node t, List<T> ret)
    {
        if (t == null) return;
        Enumerate(t.LChild, ret);
        ret.Add(t.Value);
        Enumerate(t.RChild, ret);
    }
}

平衡二分探索木ができてしまえば、setは簡単に作ることができる。

/// <summary>
/// C-like set
/// </summary>
public class Set<T> where T : IComparable
{
    protected SB_BinarySearchTree<T>.Node _root;

    public T this[int idx]{ get { return ElementAt(idx); } }

    public int Count()
    {
        return SB_BinarySearchTree<T>.Count(_root);
    }

    public virtual void Insert(T v)
    {
        if (_root == null) _root = new SB_BinarySearchTree<T>.Node(v);
        else
        {
            if (SB_BinarySearchTree<T>.Find(_root, v) != null) return;
            _root = SB_BinarySearchTree<T>.Insert(_root, v);
        }
    }

    public void Clear()
    {
        _root = null;
    }

    public void Remove(T v)
    {
        _root = SB_BinarySearchTree<T>.Remove(_root, v);
    }

    public bool Contains(T v)
    {
        return SB_BinarySearchTree<T>.Contains(_root, v);
    }

    public T ElementAt(int k)
    {
        var node = SB_BinarySearchTree<T>.FindByIndex(_root, k);
        if (node == null) throw new IndexOutOfRangeException();
        return node.Value;
    }

    public int Count(T v)
    {
        return SB_BinarySearchTree<T>.UpperBound(_root, v) - SB_BinarySearchTree<T>.LowerBound(_root, v);
    }

    public int LowerBound(T v)
    {
        return SB_BinarySearchTree<T>.LowerBound(_root, v);
    }

    public int UpperBound(T v)
    {
        return SB_BinarySearchTree<T>.UpperBound(_root, v);
    }

    public Tuple<int, int> EqualRange(T v)
    {
        if (!Contains(v)) return new Tuple<int, int>(-1, -1);
        return new Tuple<int, int>(SB_BinarySearchTree<T>.LowerBound(_root, v), SB_BinarySearchTree<T>.UpperBound(_root, v) - 1);
    }

    public List<T> ToList()
    {
        return new List<T>(SB_BinarySearchTree<T>.Enumerate(_root));
    }
}

setのInsert()で重複を許容すればmultisetになる。

/// <summary>
/// C-like multiset
/// </summary>
public class MultiSet<T> : Set<T> where T : IComparable
{
    public override void Insert(T v)
    {
        if (_root == null) _root = new SB_BinarySearchTree<T>.Node(v);
        else _root = SB_BinarySearchTree<T>.Insert(_root, v);
    }
}

HackerRank Week of Code 28 参加日記

111位/10432人でレート微増だった。Easy問題は省略する。
https://www.hackerrank.com/results/w28/yambe2002


The Great XOR

整数xが与えられたとき、以下を満たす整数aの個数を答えよ。
a XOR x > x
0 < a < x
1 <= x <= 10^9

例えばx=b10101111とすると、aの候補は
b0001nnnn
b01nnnnnn
となる(nは1と0どちらでもよい)。よって答えはb0101000になる。

public static long GetAns(long x)
{
    var ret = ~x;
    var idx = 63;
    while (idx > 0 && (x >> idx) % 2 == 0) idx--;
    return ret & (((long)1 << idx) - 1);
}


Lucky Number Eight

0-9からなる文字列s(長さn)が与えられる。sのサブシーケンスを10進数の数字とみなしたとき、これが8で割り切れるパターン数を求めよ。
1 <= n <= 200000

nが大きいのでサブシーケンスの列挙はできない。8の倍数を次のように場合分けして考える。
・1桁の8の倍数(0,8)
s[i]が該当するパターン数
・2桁の8の倍数(00, 08, ... 96)
s[i]x10+s[j] が該当するパターン数 (j>i)
・3桁以上の8の倍数
3桁の8の倍数(000, 008, ... 992)について
s[i]x100+s[j]x10+s[k] が該当するパターンを列挙(k>j>i)。
下3桁が8の倍数の数字はすべて8の倍数なので、iの位置に応じてパターン数を算出する

static long mod = 1000000007;

static void Main(string[] args)
{
    var n = Int32.Parse(Console.ReadLine());
    var s = ReadLine();
    Console.WriteLine(GetAns(s));
}

public static long GetAns(string s)
{
    long ret = 0;

    var arr = s.Select(c => Int32.Parse(c.ToString())).ToArray();

    // take 1 digit
    foreach (var c in arr)
    {
        if (c % 8 == 0)
        {
            ret++;
            ret %= mod;
        }
    }

    // take 2 digits
    ret += Get2Digits(arr);
    ret %= mod;

    // take 3+ digit
    ret += Get3Digits(arr);
    ret %= mod;

    return ret;
}

public static long Get2Digits(int[] arr)
{
    long ret = 0;
    var two_digits = Enumerable.Range(0, 13).Select(i => i * 8).ToArray();  //0-96

    foreach (var cand in two_digits)
    {
        var rightVal = cand % 10;
        var leftVal = cand / 10;
        var rightCnt = 0;

        for (int i = arr.Length - 1; i >= 0; i--)
        {
            if (arr[i] == leftVal)
            {
                ret += rightCnt;
                ret %= mod;
            }

            if (arr[i] == rightVal) rightCnt++;
        }
    }

    return ret;
}

public static long Get3Digits(int[] arr)
{
    long ret = 0;
    var three_digits = Enumerable.Range(0, 125).Select(i => i * 8).ToArray();  //0-992

    foreach (var cand in three_digits)
    {
        var ldigit = cand / 100;
        var mdigit = (cand / 10) % 10;
        var rdigit = cand % 10;
        long one_cnt = 0;
        long two_cnt = 0;

        for (int i = arr.Length - 1; i >= 0; i--)
        {
            if (arr[i] == ldigit)
            {
                ret += ModPow(2, i, mod) * two_cnt;
                ret %= mod;
            }
            if (arr[i] == mdigit)
            {
                two_cnt += one_cnt;
                two_cnt %= mod;
            }
            if (arr[i] == rdigit) one_cnt++;
        }
    }

    return ret;
}


The Value of Friendship

1~nで番号付けされたn人の生徒がいる。また、xとyが友達になることを示すペア(x, y)がm個あたえられる。さらに
・xとyが友達のときは、yとxも友達
・xとyが友達でyとzが友達のときは、xとzも友達
とする。
それぞれの生徒の友達の数をすべて合計したものをtotalと呼ぶ。友達になる順番は任意に変えることができる。友達ペアを適用するたびにtotalを求めたとき、その合計の最大を求めよ。
1 <= q <= 16 (クエリ数)
1 <= n <= 10^5
1 <= m <= min(n(n-1)/2, 2 x 10^5)

すべての友達情報を反映させたあと、Totalに影響が少ない順(友達グループの小さい順)に1人ずつ友達から外していって計算する。このとき、閉ループを作る友達情報は先に計算する。グループ情報の保持や閉ループ検出にはUnionFindを使えばよい。

public static long GetAns(int n, List<Tuple<int, int>> friends)
{
    long ret = 0;

    var unionFind = new UnionFind();

    // 閉ループになる友達情報
    var relationCreatingClosedLoopNum = 0;
    foreach (var f in friends)
    {
        if (unionFind.IsSameGroup(f.Item1, f.Item2))
        {
            relationCreatingClosedLoopNum++;
            continue;
        }
        unionFind.Unite(f.Item1, f.Item2);
    }

    var groups = unionFind.GetGroups().Where(v => v != 0).Select(v => v + 1).ToList();
    groups.Sort();
    groups.Reverse();

    long currentVal = 0;
    foreach (var g in groups)
    {
        currentVal += g * (g - 1);
    }

    // 閉ループの分は、情報を削除してもトータルが変わらない
    ret = currentVal;
    ret += currentVal * relationCreatingClosedLoopNum;

    while (groups.Count() > 0)
    {
        // 小さいグループから処理
        var num = groups.Last();
        groups.RemoveAt(groups.Count() - 1);
        
        // 該当グループ以外の分
        currentVal -= num * (num - 1);
        ret += currentVal * (num - 1);

        // 該当グループの分
        num--;
        while (num > 1)
        {
            ret += num * (num - 1);
            num--;
        }
    }

    return ret;
}

public class UnionFind
{
    private class Node
    {
        public Node Parent { get; set; }
        public int Rank { get; set; }
        public HashSet<Node> Children;

        public Node()
        {
            Children = new HashSet<Node>();
        }
    }

    private Dictionary<object, Node> _dict = new Dictionary<object, Node>();

    private Node Root(object data)
    {
        if (!_dict.ContainsKey(data))
        {
            var node = new Node();
            _dict.Add(data, node);
            return node;
        }
        else
        {
            var node = _dict[data];
            return Find(node);
        }
    }

    private Node Find(Node node)
    {
        if (node.Parent == null) return node;
        return node.Parent = Find(node.Parent);
    }

    public void Unite(IComparable x, IComparable y)
    {
        var xRoot = Root(x);
        var yRoot = Root(y);
        if (xRoot == yRoot) return;

        if (xRoot.Rank < yRoot.Rank)
        {
            ChangeParent(yRoot, xRoot);
        }
        else
        {
            ChangeParent(xRoot, yRoot);
            if (xRoot.Rank == yRoot.Rank) xRoot.Rank++;
        }
    }

    void ChangeParent(Node parent, Node childRoot)
    {
        if (childRoot.Parent != null) childRoot.Parent.Children.Remove(childRoot);

        childRoot.Parent = parent;
        childRoot.Parent.Children.Add(childRoot);

        foreach (var child in childRoot.Children.ToList())
        {
            ChangeParent(parent, child);
        }
    }

    public bool IsSameGroup(IComparable x, IComparable y)
    {
        return Root(x) == Root(y);
    }

    public List<long> GetGroups()
    {
        var ret = new List<long>();

        foreach (var r in _dict.Values.Where(d => d.Parent == null))
        {
            ret.Add(r.Children.Count());
        }

        return ret;
    }
}


Choosing White Balls

横一列に並んだn個のボールがある。ボールは黒か白に色がついている。このとき、k回ボール拾い上げて、なるべく多くの白ボールを拾いたい。拾い上げi回目について(1 <= i <= k)、
・1からn-i+1までの数字xを選ぶ
・端からx番目のボールを拾う。端は左右どちらでもよい
の条件がある。最善手を取ったときの、白ボールの個数の期待値を求めよ。
1 <= k <= n < 30

メモ化再帰で解くだけけだが、本番ではいくつかのケースでTLEになっていた。Dictionaryのキーチェック・値取得を行う部分で

if (dict.ContainsKey(key)) val = dict[key];

のようにしていたのが非効率だった。

dict.TryGetValue(key, out val);

このようにTryGetValue()を使ったほうが、内部的な検索が一回で済むので早いようだ。
参考:
http://stackoverflow.com/questions/9382681/what-is-more-efficient-dictionary-trygetvalue-or-containskeyitem
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,2e5bc6d8c0f21e67

public static double GetAns(int n, int k, string s)
{
    _n = n;
    _memo.Clear();

    var state = GetState(s);
    state = (int)Flip(state, n);
    return GetAns(k, state);
}

static long Flip(long balls, int n)
{
    var balls2 = Reverse32(balls) >> (32 - n);
    return Math.Min(balls, balls2);
}
        
public static long Reverse32(long value)
{
    var n = value >> 16 | value << 16;
    n = n >> 0x8 & 0x00ff00ff | n << 0x8 & 0xff00ff00;
    n = n >> 0x4 & 0x0f0f0f0f | n << 0x4 & 0xf0f0f0f0;
    n = n >> 0x2 & 0x33333333 | n << 0x2 & 0xcccccccc;
    n = n >> 0x1 & 0x55555555 | n << 0x1 & 0xaaaaaaaa;
    return n;
}

public static int GetState(string s)
{
    var ret = 0;

    for (int i = 0; i < s.Length; i++)
    {
        ret <<= 1;
        ret |= (s[i] == 'W') ? 1 : 0;
    }

    return ret;
}

public static long Pack(int n, int k, long state)
{
    return state + ((long)n << 32) + ((long)k << 40);
}

static Dictionary<long, double> _memo = new Dictionary<long, double>();

public static double GetAns(int k, long state)
{
    return Dfs(_n, k, state);
}

public static double Dfs(int n, int k, long state)
{
    if (state == 0) return 0;
    if (k == 0) return 0;

    state = Flip(state, n);

    double ret = 0;
    var packed = Pack(n, k, state);

    // これだとTLEになる・・・
    //if (_memo.ContainsKey(packed)) return _memo[packed];

    // こっちのほうが早いようだ
    double result;
    if (_memo.TryGetValue(packed, out result))
        return result;

    double cnt = 0;
    double sum = 0;

    // 1 <= x <= n - i + 1
    for (int x = 1; x <= n; x++)
    {
        // left
        var bit = 1L << x - 1;
        var ball1 = Remove(state, x - 1);
        var sum_left = Dfs(n - 1, k - 1, ball1) + ((state & bit) != 0 ? 1 : 0);

        // right
        bit = 1L << (n - 1 - x + 1);
        var ball2 = Remove(state, n - 1 - x + 1);
        var sum_right = Dfs(n - 1, k - 1, ball2) + ((state & bit) != 0 ? 1 : 0);

        sum += Math.Max(sum_left, sum_right);

        cnt++;
    }
    ret = sum / cnt;

    _memo.Add(packed, ret);

    return ret;
}

static long Remove(long balls, int i)
{
    var bit = 1L << i;
    balls &= ~bit;
    var mask = bit - 1;
    return (balls & mask) | (balls & ~mask & long.MaxValue) >> 1;
}


Suffix Rotation

英小文字からなる文字列sがある。各ターンで以下の作業ができるとき、最小何ターンで辞書順最小の文字列に変換することができるか答えよ。
・任意のインデックスiを選ぶ。iは過去に選ばれたインデックスより大きくなければならない
・1回かそれ以上、i以降の文字列をローテートする。方向は左右どちらでもよい
1 <= g <= 100 (sの個数)
1 <= |s| <= 1000

DPで求めることができるが、次の考慮が必要になる。

・ローテーションの作業をインデックスで持つ
たとえばIndex=0の位置で左に3つローテーションする場合、実際に回すと以下のようになるが
abcdef -> defabc (ローテーション)
次のようにインデックス位置の移動で表すことが可能。
abcdef -> abcdef (インデックス位置)
^-----------^---

・小さい文字から削除して処理
sを直接1文字ずつ処理するループ回数が膨大になるが、文字種ごとに処理すれば外のループは26回で済むようになる。さらに、たとえばsがbaacaaadaeでaを削除することを考えると、どの順番で行おうが残る文字列はbcdeになる(ターン回数は3)。そして、次のインデックスはbcdeの位置(0,3,7,9)のいずれかになる。

これらを考慮すると、注目している文字と現在のインデックスだけ保持すればよいことが分かる。
(実装は省略)

HackerRank Week of Code - 26 参加日記

398位/6951人でレート減してしまった。いつも数学の問題が多いと順位を落としてしまう。以下Easy問は省略。


Twins

整数iとjがいずれも素数、かつ距離が2のとき、これらをペアであるとする。整数nとmが与えられたとき、区間[n,m]にはいくつのペアが存在するか。ただし、(i, j)と(j, i)は同じペアと扱う。
1 <= n <= m <= 10^9
m - n <= 10^6

あらかじめエラトステネスの篩を使って区間内の素数を列挙し、あとはi=n..m-2でループしてiとi+2が両方素数の場合をカウントすればよい。

// n, m <= 10^9, m - n <= 10^6
public static int GetAns(int n, int m)
{
    var isPrime = new bool[m - n + 1];
    SegSieve(n, m + 1, isPrime);
    var ret = 0;
    for (int i = n; i <= m - 2; i++)
    {
        if (isPrime[i - n] && isPrime[i - n + 2]) ret++;
    }
    return ret;
}

// returns primes in [a, b)
public static void SegSieve(int a, int b, bool[] isPrime)
{
    for (int i = 0; i < isPrime.Length; i++) isPrime[i] = true;
    for (int i = 0; i < isPrime.Length; i++)
    {
        if (i + a < 2) isPrime[i] = false;
    }

    var sqrtb = (int)Math.Ceiling(Math.Sqrt(b));

    // prime table of [0, sqrt[b])
    var primeTable_0_b = new int[sqrtb];
    var primeTable_0_b_is = new bool[sqrtb];

    Sieve(primeTable_0_b, primeTable_0_b_is);

    foreach (var p in primeTable_0_b)
    {
        if (p == -1) continue;
        var st = (a / p) * p;
        if (st == 0) st = p;
        if (st == p) st += p;
        while (st < b)
        {
            if (st >= a) isPrime[st - a] = false;
            st += p;
        }
    }
}

public static void Sieve(int[] prime, bool[] isPrime)
{
    for (int i = 0; i < prime.Length; i++) prime[i] = -1;
    for (int i = 0; i < isPrime.Length; i++) isPrime[i] = true;
    isPrime[0] = isPrime[1] = false;

    var idx = 0;
    for (int i = 2; i < isPrime.Length; i++)
    {
        if (isPrime[i])
        {
            prime[++idx] = i;
            for (int j = 2 * i; j < isPrime.Length; j += i) isPrime[j] = false;
        }
    }
}


Music on the Street

無限に長いストリート上に、ポイントa_0、a_1、... a_nが設置されている。a_0より前のストリートではある曲のジャンルが、a_0とa_1の間ではまた別の曲のジャンルが演奏されている(a_nからあとも同様)。ある人がある地点からポイントが大きくなる方向に時速1マイルでmマイル歩くとき、同じジャンルの曲をh_min時間以上、h_max時間以内だけ聞きたい。これを満足するスタート地点のいずれかを求めよ。
1 <= n <= 10^6
abs(a_i) <= 10^9、a_iは昇順で与えられる
1 <= h_min <= h_max <= 10^9
1 <= m <= 10^9

スタート地点の候補は限られているので、そのすべてで条件に合致するかトライする。予めボーダー間がすべて条件を満たすかどうかを管理する区間木を作っておけばよい。また、歩き始めのIndexや終点のIndexを求めるにはLowerBoundとUpperBoundも必要になる。
本番ではRmq内の配列サイズが足りず、1問RLEになっていた。

class Program
{
    public static long n;
    public static List<long> a;
    public static long m;
    public static long hmin;
    public static long hmax;

    public static Rmq _rmq;

    static void Main(string[] args)
    {
        n = long.Parse(Console.ReadLine());
        a = ReadLine().Split().Select(v => long.Parse(v)).ToList();
        var str = Console.ReadLine().Split();
        m = long.Parse(str[0]);
        hmin = long.Parse(str[1]);
        hmax = long.Parse(str[2]);
        Console.WriteLine(GetAns());
    }

    public static long GetAns()
    {
        // from left
        for (int i = 0; i < a.Count(); i++)
        {
            if (Ok(a[i] - hmin))
            {
                return a[i] - hmin;
            }
            if (Ok(a[i] - hmax))
            {
                return a[i] - hmax;
            }
            if (Ok(a[i]))
            {
                return a[i];
            }
        }

        //from right
        for (int i = a.Count() - 1; i >= 0; i--)
        {
            if (Ok(a[i] + hmin - m))
            {
                return a[i] + hmin - m;
            }
            if (Ok(a[i] + hmax - m))
            {
                return a[i] + hmax - m;
            }
            if (Ok(a[i] - m))
            {
                return a[i] - m;
            }
        }

        return -1000;
    }

    public static bool Ok(long pos)
    {
        var stIdx = (int)UpperBound(a, pos);
        if (stIdx >= a.Count() - 1) stIdx = a.Count() - 1;
        if (stIdx < 0) stIdx = 0;

        // check current pos to next bound
        if (!OkDist(pos, a[stIdx])) return false;

        var edIdx = (int)LowerBound(a, pos + m);
        edIdx--;
        if (edIdx >= a.Count() - 1) edIdx = a.Count() - 1;
        if (edIdx < 0) edIdx = 0;

        // check last bound to last pos
        if (!OkDist(a[edIdx], pos + m)) return false;

        // check intervals are all OK
        return OkIntervals(stIdx, edIdx);
    }

    public static bool OkIntervals(int idx1, int idx2)
    {
        if (idx1 == idx2) return true;
        if (idx1 + 1 == idx2) return OkDist(a[idx1], a[idx2]);

        if (_rmq == null)
        {
            _rmq = new Rmq();
            _rmq.Init((int)n + 1);

            for (int i = 0; i < n - 1; i++)
            {
                if (!OkDist(a[i], a[i + 1]))
                {
                    _rmq.Update(i, 0);
                }
                else
                {
                    _rmq.Update(i, 1);
                }
            }
        }
        return _rmq.Query(idx1, idx2) == 1;
    }

    public static bool OkDist(long pos1, long pos2)
    {
        var gap = pos2 - pos1;
        if (gap == 0) return true;
        return gap >= hmin && gap <= hmax;
    }

    public static long LowerBound(List<long> ar, long val)
    {
        long lb = -1;
        long ub = ar.Count();

        while (ub - lb > 1)
        {
            var mid = (lb + ub) / 2;
            if (ar[(int)mid] >= val)
            {
                ub = mid;
            }
            else
            {
                lb = mid;
            }
        }

        return ub;
    }

    public static long UpperBound(List<long> ar, long val)
    {
        long lb = -1;
        long ub = ar.Count();

        while (ub - lb > 1)
        {
            var mid = (lb + ub) / 2;
            if (ar[(int)mid] <= val)
            {
                lb = mid;
            }
            else
            {
                ub = mid;
            }
        }

        return lb + 1;
    }
}

public class Rmq
{
    const int MAX_N = 2000005;
    int _n;
    int[] _dat = new int[2 * MAX_N - 1];

    public void Init(int n)
    {
        _n = 1;
        while (_n < n) _n *= 2;
        for (int i = 0; i < _dat.Length; i++) _dat[i] = Int32.MaxValue;
    }

    public void Update(int k, int a)
    {
        k += _n - 1;
        _dat[k] = a;
        while (k > 0)
        {
            k = (k - 1) / 2;
            _dat[k] = Math.Min(_dat[k * 2 + 1], _dat[k * 2 + 2]);
        }
    }

    public int Query(int a, int b)
    {
        return Query(a, b, 0, 0, _n);
    }

    int Query(int a, int b, int k, int l, int r)
    {
        if (r <= a || b <= l) return Int32.MaxValue;
        if (a <= l && r <= b) return _dat[k];
        else
        {
            var vl = Query(a, b, k * 2 + 1, l, (l + r) / 2);
            var vr = Query(a, b, k * 2 + 2, (l + r) / 2, r);
            return Math.Min(vl, vr);
        }
    }
}


Satisfactory Pairs

正の整数nが与えられたとき、ax + bx = n に解が存在する(a, b)のパターン数を求めよ(a < b、x,yは正の整数)。
4 <= n <= 3x10^5

axでイテレートし、その内部でax・byの約数の組み合わせを作って、該当するものを合算していけばよい。

public static int GetAns(int n)
{
    // 使う約数を準備
    var divs = new List<int>[n + 1];
    for (int i = 1; i <= n; i++)
    {
        for (int j = i; j <= n; j += i)
        {
            if (divs[j] == null) divs[j] = new List<int>();
            divs[j].Add(i);
        }
    }

    int ct = 0;
    for (int xa = 1; xa < n; xa++)
    {
        int yb = n - xa;
        foreach (var a in divs[xa])
        {
            foreach (var b in divs[yb])
            {
                var x = xa / a;

                // x <= b / gcd(a, b) は
                // 同じ(a,b)ペアでxが最小のもののみをカウント
                if (a < b && x <= b / gcd(a, b))
                {
                    ct++;
                }
            }
        }
    }

    return ct;
}

public static int gcd(int a, int b)
{
    while (b > 0)
    {
        int c = a;
        a = b;
        b = c % b;
    }
    return a;
}

Facebook Hacker Cup 2017 Qualification Round 参加日記

○○×で予選通過。R1は時間的に参加できるかどうか微妙だ。


Progress Pie
進捗パイチャートと点Xがあたえられたとき、Xがチャートの色付き部分に入っているかどうかを判定する。
点の位置とチャートの進捗p(%)を0%からの角度に変換して求めた。

public static bool GetAns(int p, int x, int y)
{
    x -= 50;
    y -= 50;

    // 特殊なケース
    if (x == 0 && y == 0) return p > 0;
    if (p == 0) return false;

    // 中心から遠すぎる
    if (x * x + y * y > 2500) return false;

    var theta = Math.PI * 2 * (p / 100.0);

    // 垂直線(0%線)と中心-点Xの角度を求め、象限ごとに処理する
    var pt = new Pt(x, y);
    var theta_pt = Math.Asin(pt.Cross(_zero) / (pt.Norm() * _zero.Norm()));
    if (x < 0 && y < 0) //3rd
    {
        theta_pt = Math.PI - theta_pt;
    }
    else if (x < 0) //4th
    {
        theta_pt = Math.PI * 2 + theta_pt;
    }
    else if (y < 0) //2nd
    {
        theta_pt = Math.PI - theta_pt;
    }

    return theta_pt <= theta;
}


Lazy Loading
重さの異なるN個の荷物を、なるべく多くの回数で運ぶ。ただし一回50ポンド以上は運ばなければならない。また、まとめて運んでいる荷物は、その中で一番重い荷物x個数でトータルの重さと嘘をつくことができる。
残っている荷物で最も重いものを1つと、軽いほうから50ポンドと虚偽できるまで運ぶのを繰り返せばよい。

public static int GetAns(List<int> w)
{
    var ret = 0;

    while (w.Count() > 0)
    {
        w.Sort();
        if (w.Count() * w[w.Count() - 1] < 50) break;
        w = w.Skip((int)Math.Ceiling((double)50 / (double)w[w.Count() - 1]) - 1).ToList();
        w.Remove(w.Last());
        ret++;
    }

    return ret;
}


Fighting the Zombie
部分的には、Y面のサイコロをN回振ったとき、その合計がある数N以上になる確率を求める問題になる。
dp[i, j]をi回振って合計がjになる確率と定義すれば、
dp[i + 1, j + 1] += dp[i, j] / N
dp[i + 1, j + 2] += dp[i, j] / N
...
dp[i + 1, j + N] += dp[i, j] / N
と更新して最終的な確率がすべて求められる。
(実装は省略)