Avant-Garde Code

アバンギャルド・コード is 前衛的算譜.

ハイパー LINQ 集 (AtCoder 2020年1月)

2020年1月の AtCoder の問題を、LINQ (C#) を使って解いていきます。全8問。
問題文はそれぞれのリンク先を参照してください。

曲のリストを列挙し、曲名が X に一致するまでスキップします。
その一致した曲1つをスキップして、残りの再生時間の和を求めます。

using System;
using System.Linq;

class A
{
    static void Main()
    {
        var n = int.Parse(Console.ReadLine());
        var ms = new int[n].Select(_ => Console.ReadLine().Split()).ToArray();
        var x = Console.ReadLine();
        Console.WriteLine(ms.SkipWhile(m => m[0] != x).Skip(1).Sum(m => int.Parse(m[1])));
    }
}

本来は LINQ を使わずに解く問題なのですが、前問と同様にして解けます。
文字 a ~ z を列挙し、C に一致するまでスキップします。
その一致した文字1つをスキップして、最初に現れる文字を求めます。

using System;
using System.Linq;

class A
{
    static void Main()
    {
        var c = Console.ReadLine()[0];
        Console.WriteLine(Enumerable.Range('a', 26).Select(x => (char)x).SkipWhile(x => x != c).Skip(1).First());
    }
}

N * M から得点の合計を引いた値が最後のテストで必要となる点数です。
この値が K 以下であれば達成可能で、負である場合は 0 点でも達成可能です。

using System;
using System.Linq;

class B
{
    static int[] Read() => Console.ReadLine().Split().Select(int.Parse).ToArray();
    static void Main()
    {
        var h = Read();
        int n = h[0], k = h[1], m = h[2];
        var x = n * m - Read().Sum();
        Console.WriteLine(x <= k ? Math.Max(x, 0) : -1);
    }
}

入力を問題 ID でグループ化して提出結果に "AC" が含まれるものだけに絞り込み先頭から "WA" が続く回数を求めます。
このグループ数が正答数を、回数の和がペナルティ数を表します。

using System;
using System.Linq;

class C
{
    static void Main()
    {
        var h = Console.ReadLine().Split().Select(int.Parse).ToArray();
        var gs = new int[h[1]].Select(_ => Console.ReadLine().Split()).GroupBy(x => x[0], x => x[1]).Where(g => g.Contains("AC")).Select(g => g.TakeWhile(x => x == "WA").Count()).ToArray();
        Console.WriteLine($"{gs.Length} {gs.Sum()}");
    }
}

LINQ で簡潔に記述できる問題の中では難易度が高めです。
SK 個並べ、その後ろに  10^{9}N - K 個並べれば条件を満たします。
ただし、  S=10^{9} の場合は後ろに 1 を並べます。

using System;
using System.Linq;

class C
{
    static void Main()
    {
        var h = Console.ReadLine().Split().Select(int.Parse).ToArray();
        int n = h[0], k = h[1], s = h[2], M = 1000000000;

        var a = Enumerable.Repeat(s, k).Concat(Enumerable.Repeat(s == M ? 1 : M, n - k));
        Console.WriteLine(string.Join(" ", a));
    }
}

本来は LINQ を使わずに解く問題だと思いますが、別解として載せておきます。LINQ 力高め。
 P_1, \cdots , P_j の最小値を順に求めて  M_j に格納していきます。
そして  P_i \leq M_i を満たす i の個数を求めます。

using System;
using System.Linq;

class C
{
    static void Main()
    {
        var n = int.Parse(Console.ReadLine());
        var p = Console.ReadLine().Split().Select(int.Parse).ToArray();

        var m = new int[n];
        var j = 0;
        p.Aggregate(int.MaxValue, (a, x) => m[j++] = Math.Min(a, x));
        Console.WriteLine(Enumerable.Range(0, n).Count(i => p[i] <= m[i]));
    }
}

必殺技のダメージ量の和が H 以上となれば勝ちです。

using System;
using System.Linq;

class B
{
    static int[] Read() => Console.ReadLine().Split().Select(int.Parse).ToArray();
    static void Main()
    {
        var p = Read();
        var h = p[0];
        Console.WriteLine(Read().Sum() >= h ? "Yes" : "No");
    }
}

モンスターの体力を降順に並べ替えて先頭から K 回必殺技でスキップして残りの和を求めます。

using System;
using System.Linq;

class C
{
    static long[] Read() => Console.ReadLine().Split().Select(long.Parse).ToArray();
    static void Main()
    {
        var p = Read();
        var k = (int)p[1];
        Console.WriteLine(Read().OrderByDescending(x => x).Skip(k).Sum());
    }
}

バージョン

  • C# 8.0
    • .NET Core 3.1.201 で提出
    • 現行の AtCoder では、3種類の C# を選べる
    • 今回のコードは C# 6 でも可

競技プログラミング典型問題集

競技プログラミングにおける典型問題は、新たに学習したアルゴリズムを実装して試すとき、
あるいはライブラリを作成して試すときなどに利用すると最適です。
これまでに経験した範囲で、分野ごとに典型問題と思われるものを以下にまとめました。
AtCoder の緑~水ランク前後の人であれば利用する機会が多いのではないでしょうか。

サイト

典型問題が多く出題されたコンテスト

アルゴリズム

データ構造

最長増加部分列 (LIS)

動的計画法 (DP)

bit DP

木 (Tree)

Union-Find 木

優先度付きキュー (priority queue)

区間木 (セグメント木, segment tree)

最短経路問題

深さ優先探索 (DFS) および幅優先探索 (BFS)

Dijkstra

巡回セールスマン問題

最大流問題

最大流・最小カット

二部マッチング

グラフ (その他)

最小全域木

Euler Tour

ビット演算で減算を実装する【アルゴリズム実技検定 解説】

第二回 アルゴリズム実技検定 (2020年4月) の問題 A - エレベーター のコードをいくつか書きましょう。

atcoder.jp

問題文 (抜粋)

下から順に B9, B8, ..., B1, 1F, 2F, ..., 9F と呼ばれる 18 のフロアを持つ建物があります。
エレベーターが S から T へ移動するには最短で何秒を要するでしょうか。

問題文から、減法演算 (引き算、差) を使って解けそうだと判断できます。
実際の仕事現場であれば減法演算子 - を利用するだけでしょう。

しかし、アルゴリズム実技検定 (PAST) では

1からプログラムを作成する能力を問う

と標榜しているわけですから、
そんな飛び道具に頼った実装は許されないと容易に想像できます。
すなわち、ビット演算 (AND、OR、XOR、NOT、シフト、等値比較) を駆使して
この問題に取り組むのです。

その前にまず、普通に実装したときのソースコードを示しておきましょう。
前回に引き続き、使用言語は C# です。
B1 を -1、1F を 0、2F を 1、・・・と考えれば次のように書けます。

using System;

class A
{
    static void Main()
    {
        var f = Array.ConvertAll(Console.ReadLine().Split(), s => s[1] == 'F' ? s[0] - '1' : '0' - s[1]);
        Console.WriteLine(Math.Abs(f[0] - f[1]));
    }
}

上記のコードには、減算が3回のほか、絶対値 Math.Abs が1回含まれています。
これらをビット演算で置き換えます。

前回 (加算) と同様に、再帰を使って減算のコードを書いてみます。
使用言語は C# ですが、他のプログラミング言語でも同様のことができると思います。

using System;

class A
{
    static void Main()
    {
        var f = Array.ConvertAll(Console.ReadLine().Split(), s => s[1] == 'F' ? Subtract(s[0], '1') : Subtract('0', s[1]));
        Console.WriteLine(Abs(Subtract(f[0], f[1])));
    }

    static int Subtract(int x, int y)
    {
        var xor = x ^ y;
        var carry = (xor & y) << 1;
        return carry == 0 ? xor : Subtract(xor, carry);
    }

    static int Abs(int x) => (x & 1 << 31) == 0 ? x : ~Subtract(x, 1);
}

carry は繰り下げを表しています。

1 << 31int.MinValue と同義であり、コンパイル時に定数として埋め込まれます。
その値は  -2^{31} (16進数表記では 80000000) です。

2の補数 (-1倍) については、「1の補数に1を足したもの」の代わりに
「1を引いたものの1の補数」としています。

なお、x - yx + (-y) と考えれば、
前回 (加算) で作成した Add 関数を用いて Add(x, Add(~y, 1)) としてもよいでしょう。

これで真の AC (正解) が得られました。

AC

また、再帰を使わず、whilefor を使って次のようにも書けます。

using System;

class A
{
    static void Main()
    {
        var f = Array.ConvertAll(Console.ReadLine().Split(), s => s[1] == 'F' ? Subtract(s[0], '1') : Subtract('0', s[1]));
        Console.WriteLine(Subtract(f[0], f[1]).ToString().Replace("-", ""));
    }

    static int Subtract(int x, int y)
    {
        while (y != 0) y = ((x ^= y) & y) << 1;
        return x;
    }
}

Subtract の中で変数を消費していません。
さらに、文字 - を除去することで絶対値を得ています。

前回: ビット演算で加算を実装する

バージョン

  • C# 8.0
    • Mono-csc 3.5.0 で提出
    • 現行の AtCoder では、3種類の C# を選べる
    • 今回のコードは C# 6 でも可

参照

ビット演算で加算を実装する【アルゴリズム実技検定 解説】

第一回 アルゴリズム実技検定 (2019年12月) の問題 A - 2 倍チェック のコードをいくつか書きましょう。

atcoder.jp

問題文 (抜粋)

S が 3 桁の整数である場合 (0 で始まる場合を含む) はその 2 倍の値を出力し、
そうでなければ error と出力するプログラムを作成せよ。

2 倍の値を求める部分については、普通に考えれば 2 * n あるいは n + n のような
四則演算のコードを書くだけにも見えます。

しかし、アルゴリズム実技検定 (PAST) のキャッチフレーズは

1からプログラムを作成する能力を問う

です。
したがって、そんな単純な実装で許されるはずはなく、
出題者の想定解法は「ビット演算を使って四則演算を実装する」です (断言)。
すなわち、AND、OR、XOR、NOT、シフト、等値比較を駆使してこの問題を解くのです。

というわけで以下では、ビット演算で加法演算 (足し算、和) を自作し、
n + n と同等のコードを実現していきます。
使用言語は C# ですが、他のプログラミング言語でも同様のことができると思います。

using System;

class A
{
    static void Main()
    {
        int n;
        Console.WriteLine(int.TryParse(Console.ReadLine(), out n) ? $"{Add(n, n)}" : "error");
    }

    static int Add(int x, int y)
    {
        var xor = x ^ y;
        var carry = (x & y) << 1;
        return carry == 0 ? xor : Add(xor, carry);
    }
}

上記のように、再帰でシンプルに書けます。 carry は繰り上げを表しています。
これで真の AC (正解) が得られました。

AC

また、再帰を使わず、whilefor を使って次のようにも書けます。

using System.Linq;
using static System.Console;

class A
{
    static void Main() => Solve(ReadLine());
    static void Solve(string s) => WriteLine(s.All(char.IsNumber) ? $"{Twice(int.Parse(s))}" : "error");
    static int Twice(int x) => Add(x, x);

    static int Add(int x, int y)
    {
        while (y != 0)
        {
            var carry = (x & y) << 1;
            x ^= y;
            y = carry;
        }
        return x;
    }
}

ちなみに、この問題の場合は 2 倍するだけなので、
実は左シフト演算 n << 1 で達成できます。

using System;

class A
{
    static void Main()
    {
        int n;
        Console.WriteLine(int.TryParse(Console.ReadLine(), out n) ? $"{n << 1}" : "error");
    }
}

次回: ビット演算で減算を実装する

バージョン

  • C# 6
    • 2020年3月以前の AtCoder では Mono 4.6.2

参照

About This Blog

This Blog's Concept

In programming, the solution is not always unique for something you want to achieve.

I usually try multiple implementations when I find them.
In competitive programming such as Codeforces and AtCoder, I think that submitting many solutions is one of the ways to enjoy.
If you have experienced many types of patterns in practical tasks, you can think of 3 or more solutions for a frequent problem.

So in this blog, I will show multiple solutions for the target problem, and also implement primitive functions.

このブログについて

このブログのコンセプト

プログラミングにおいては、何か実現したい対象があったとして、それを実現する方法は一つだけとは限りません。

筆者は普段から、複数の実装方法を見つけた場合には試すようにしています。
AtCoderCodeforces のような競技プログラミングにおいては、多くの解法を提出することも楽しみ方の一つと考えています。
実務でも頻出のパターンで類型を多く経験していると、だいたい3通り以上の解法が思い浮かぶものです。

そこでこのブログでは、対象の問題に対して複数の実装方法を示したり、原始的な機能を自作したりしていきます。