Hatena::Grouptopcoder

cafelier@SRM

cafelier のSRM参加記録です。コンテスト中に考えてたことを執拗に全部書き残すとどうなるだろうかという試み本番中にこういうコードが書きたかったなあ、という後で書いた反省コードを書き残す試み

スパムが来たのでしばらくコメント欄をはてなユーザ限定にしています、すみません、

 | 

2012-04-16

競技プログラミングのためのD言語 (2/2)

03:22 | はてなブックマーク - 競技プログラミングのためのD言語 (2/2) - cafelier@SRM

続きです。

foreach文

string[] strs = ["aaa", "bbb", "ccc"];

foreach(s; strs) // strsの要素をループ
   writeln(s);
foreach(i, s; strs) // strsの要素を添え字付きでループ
   writeln(i, ": ", s);
foreach(i; 0..100) // 0から99まで(100は含まない)ループ
   writeln(i);

foreach_reverse(s; strs) // strsの要素を逆順にループ
   writeln(s);
foreach_reverse(i, s; strs) // strsの要素を添え字付きで逆順にループ
   writeln(i, ": ", s);
foreach_reverse(i; 0..100) // 99から0までループ
   writeln(i);

はい。


基本的ではない型・演算

標準ライブラリの std.bigint というモジュールを import すると BigInt 型が使えます。普通に+-*/で演算できます。JavaのprobablePrimeのような格好いいメソッドはありません。

import std.stdio;
import std.bigint;

BigInt factorial(int n) {
   return n>0 ? factorial(n-1) * n : BigInt(1);
}

void main()
{
   writeln(factorial(1000));
}

複素数が言語組み込みの型としてあります。cfloat, cdouble, creal という型名です。std.complexというライブラリ実装に置き換えるという話があったんですがまだ置き換わってないので組み込み型の方を紹介します。

import std.stdio;
import std.math; // 実部 .re と虚部 .im と四則演算以外の関数は std.math で
void main()
{
   creal z = 1+2i;
   z = z*z; // (1+2i)(1+2i) = -3+4i
   writeln(z.re); // -3
   writeln(z.im); // 4
   writeln(conj(z)); // -3+-4i
}

普通の整数や浮動小数点数にもちょっと便利機能があります。

void main()
{
   int x = 3^^4; // 3の4乗 : 81
   int veryLarge = int.max; // intに入る最大値すなわち2147483647
   real inf = real.inf; // IEEE754の∞値
}

競技プログラミング的に言うと、べき乗は3状態あるもののビットじゃないけどビットマスク、っぽいものの計算が楽になったりします。.max や .min や .inf は変数の初期値として埋めておくと役に立つ場面が色々。

詳しくはこちら

データ構造:連想配列

std::map<string, int> や java.util.HashMap<String, Integer> のような、あれです。メモ化に使います。

int[string] m;
m["one"] = 1;
m["two"] = 2;
m["three"] = 3;
if( "four" in m ) writeln("hogehoge");
m.remove("three");
writeln(m.length); // 2
foreach(key, value; m)
   { ... }
int[string] point_to_same_as_m = m; // 同じmapを参照
int[string] another = m.dup; // コピーはdupで

そんな感じで ValueType[KeyType] という型として言語組み込みでこの機能を持っています。実装はハッシュ表を使っているので、C++のstd::mapやjavaのTreeMapのような、キーの順序通りに取り出す機能は、この連想配列にはありません。

HashSet相当のものはないので、私は bool[Type] を set としてよく使っています。

詳しくはこちら

Associative Arrays: 英語 日本語


データ構造:その他

std.container

に、BinaryHeap (priority_queue/PriorityQueue) と RedBlackTree (set/TreeSet) があります。私使ったことないので詳しくは知りません...。

詳しくはこちら

std.container: 英語 日本語


データ構造:pair, tuple

std.typecons モジュールに、Tuple!() というテンプレートが用意されています。2つ組や3つ組を作れます。

import std.typecons;

void main()
{
  Tuple!(int, string, real) t = tuple(1, "hello", 3.14); // tuple() 関数はC++のmake_pairみたいなもの、型を省略して構築
  int x = t[0]; // 0番目はint
  string y = t[1]; // 1番目はstring
  t[2] = 2.72; // 2番目はreal
}

DのTupleの良いところは、メンバの名前をfirstやsecondじゃなくて自由に決められるところです。

Tuple!(int, "src", int, "dest") edge = tuple(0, 2);
writeln(edge.src, "-->" edge.dest);

型名が長いなーと思ったら、aliasを使います(C++で言うtypedefです)。

alias Tuple!(real, "x", real, "y", real, "r") Circle;
Circle c = Circle(0.1, 0.2, 0.3);
c.r = 0.4;
詳しくはこちら

std.typecons: 英語 日本語


クラスの書き方・使い方

Tupleでメンバの名前がつけられるとなると、競技プログラミングレベルではclassやstructを定義せずに全部Tupleで済ませてしまうことがかなり多くなるのですが、必要ならばもちろんクラスを作って使うこともできます。クラスの定義、使い方はJavaと同じです。 唯一の注意点としては、コンストラクタの名前は クラス名() ではなく、this() です。あとデフォルトは public です。

詳しくは他のもっと真っ当な解説などを。


無名関数・クロージャ

void sort(int[] arr,  bool delegate(int x, int y) ordered) {
   foreach(i; 0..arr.length)
      foreach(j; i+1..arr.length)
         if( !ordered(i, j) )
            {int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp;}
}

void main() {
   int[] weight = [9,3,4,7,1,6,2,8,5];
   int[] arr = [0,1,2,3,4,5,6,7,8];
   sort( arr, (int x, int y){ return weight[x]<weight[y]; } );
}

(int x, int y){ return weight[x]<weight[y]; } の部分が無名の関数です。周りの環境のweightという変数を使うこともできています。この無名関数を変数で受けるには、<b>delegate という型で受けます。構文は上の例のような感じ。


アルゴリズム

C++の<algorithm>ヘッダに相当する物は、Dでは std.algorithm モジュールです。std.range も見ておくと便利です(特に、二分探索系の機能はstd.rangeにあります)。機能としては基本的に <algorithm> をなぞったものが多いですが、よく見るとBoyerMooreとかlevenshteinDistanceなどが紛れ込んでいてちょっと楽しいです。

楽しいかどうかはさておき、std.algorithm を使うにあたって重要なポイントは、Range という概念です。C++ では iterator というものの上にSTLのアルゴリズムがすべて組まれていましたが、D言語では Range の上に全部のアルゴリズムが組まれています。

なにやら難しそうですが、要はぶっちゃけると Rangeiteratorbegin と end の組と思って下さい。Topcoderなどでも

#define ALL(c) (c).begin(), (c).end()

というけしからんマクロを書いている人をよく見ますが、確かに、この二つは常に組であつかった方が色々と便利なのです。

import std.algorithm;
import std.stdio;

void main()
{
   int[] arr = [1,2,3];
   auto r = find(arr, 2);
   writeln(r); // [2,3]
}

[1,2,3] から 2 を探すと、最初に 2 が現れた位置より後ろ全体を指すRangeが返ります。別の例。count。

import std.algorithm;
import std.stdio;

void main()
{
   int[] arr = [1,2,3];
   writeln( count!((int x){return x>=2;})(arr) ); // 2以上のものの個数。2個。
   writeln( count!("a>=2")(arr) ); // 同じ意味
}

関数を引数にとるタイプのアルゴリズムの場合、アルゴリズム名!(ここ)(range) に関数を渡します。周りの環境の変数をみないのであれば、"a"を引数と思った関数のbodyの文字列を書いても行けたりします。!() は C++ でいうテンプレート引数を渡している部分になります。

普通の配列を操作する&走査するRangeは要するにただの部分配列ですが、Rangeという概念自体はもっと抽象的なものなので

import std.algorithm;
import std.range;
import std.stdio;

void main()
{
   string[] names = ["taro", "hanako", "toastman"];
   int[] ratings = [3000, 4500, 4000];

   sort( zip(ratings, names) );

   writeln(names);  // ["taro", "toastman", "hanako"]
   writeln(ratings); // [3000, 4000, 4500]
}

二つの配列を並行して走るRangeをzipでその場で作ってそのRangeをsort、とやると元の二つの配列がpairとしての順序でsortされたりします。これは割と経験的に便利です。

別の例。二分探索。

import std.array;
import std.range;

void main()
{
   int[] arr = [1,2,3,4,5];
   auto sorted = assumeSorted(arr);

   int[] lw = array(sorted.lowerBound(3)); // [1,2];
   int[] up = array(sorted.upperBound(3)); // [4,5];
}

Rangeを返値として返す関数の結果は大抵よくわからない謎の型になっていますが、auto で自動的に適切な型で受け取ることができます。あと、そのRangeをこれ以上他のアルゴリズムに渡したりしないので普通の配列にしてください、と思ったらstd.arrayのarray()関数でRangeを何でも配列化できます。


詳しくはこちら

スレッド

std.concurrencyのspawnという関数でスレッドを起動してsendとreceiveでやりとりします。最近のマシンは4コアや8コアは持っていたりするので、ローカルで実行する形式のコンテストでは、単純にテストケースを別スレッドで並行に実行するだけで8倍の高速化が達成できたりすることがあります。

import std.stdio;
import std.concurrency;

void compute_square(Tid parent, int n)
{
   send(parent, n, n*n);
}

void main()
{
   // 100個スレッド起動
   foreach(i; 0..100)
      spawn(&compute_square, thisTid, i);

   // 100個返事が来るのを待つ
   int[] results = new int[100];
   foreach(_; 0..100)
      receive( (int i, int result)
      {
         results[i] = result;
      });

   writeln(results);
}

スレッドを起動したりメッセージ通信したりするコードはこれだけです。


詳しくはこちら

そして最近はstd.parallelismなどという物もできていました…知らなかった…


まとめ

とりあえず、以上です。

D言語の言語機能の紹介としては、テンプレートとかテンプレートとかテンプレートとか契約とか契約とか契約とか特徴的な部分をごっそり省いているのですが、競技プログラミングでそこまで激しくその辺りの機能を使うことはなかろうということで省いています。「○○と同じです」で済ませた部分も本当は全然同じではなくて、同じ書き方をしたらだいたい同じに動くというだけで、同じでない書き方も色々できて面白いのですが、その辺りは他のまともな解説記事を読んでみてください。ではでは。

majiangmajiang2012/04/17 07:58除算/剰余の挙動は Python と同じですか?

cafeliercafelier2012/04/17 08:38Good question! 負の数を割った場合のお話だと思うのですが、結論ですが、Python と違います。残念ながら、使いにくい方の挙動です (^^;

D言語(余りの符号は割られる数に合わせる):
-10 / 3 == -3
-10 % 3 == -1

Python(余りは割る数の符号に合わせる):
-10 / 3 == -4
-10 % 3 == 2

仕様で未定義ということもなくて、この動作をする、と明記されている感じです。

komiyamkomiyam2012/04/17 13:37C++での int& のような参照変数を使うことはできますか?

cafeliercafelier2012/04/17 14:01完全に同じものはないです。関数の引数と、関数の返り値と、foreachのループに使う変数は、"ref" をつけると参照になります。それ以外の場所では使えません。

void set_value(ref int x) { x = 6502; }
int hoge; set_value(hoge); // hoge == 6502

class Foo {
ref int foo() { return x; }
int x;
}
(new Foo()).foo() = 8086;

int[] arr = [1,2,3];
foreach(ref x; arr) x = x*x;
// arr == [1,4,9]

http://dlang.org/function.html#parameters
http://dlang.org/function.html#ref-functions

onote2onote22012/04/17 15:15整理されていて分かりやすかったです。ありがとうございます。
ところで、連想配列の箇所で誤記かと。
KeyType[ValueType] → ValueType[KeyType]

cafeliercafelier2012/04/17 15:30直しました。ありがとうございます!

majiangmajiang2012/04/17 22:14それでは int を継承して除算の挙動を変えたいのですが、どうやればよいでしょうか?

cafeliercafelier2012/04/17 23:42Dでは組み込み型の継承はできないです。そもそも、あまり継承は使わないで、intをラップしてintを模した別の型を定義するスタイルが主流です。これをサポートする std.typecons の Proxy というライブラリがあって、

import std.conv;
import std.stdio;
import std.typecons;

// 除算の別の挙動の定義
int my_div(int x, int y) { int q = x/y; return q*y > x ? q-1 : q; }
int my_mod(int x, int y) { return x - my_div(x,y)*y; }

// 除算だけ変えたintっぽい型
struct modint
{
// コンストラクタ
this(int v){this.val = v;}
private int val;

// ほとんどの処理はintに転送
mixin Proxy!val;

// modint/int, modint%int の挙動を変更
auto opBinary(string op:"/", T)(T y) { return modint(my_div(val, cast(int)y)); }
auto opBinary(string op:"%", T)(T y) { return modint(my_mod(val, cast(int)y)); }

// int/modint, int%modint の挙動を変更
auto opBinaryRight(string op:"/", T:int)(T y) { return modint(my_div(cast(int)y, val)); }
auto opBinaryRight(string op:"%", T:int)(T y) { return modint(my_mod(cast(int)y, val)); }

// +-* なども、挙動変更後の型を返すように置き換え
auto opBinary(string op, T)(T y) { return modint(mixin("val"~op~"cast(int)y")); }
auto opBinaryRight(string op, T:int)(T y) { return modint(mixin("cast(int)y"~op~"val")); }
}

void main()
{
modint x = -10;
writeln(x / 3); // -4
writeln(x % 3); // 2
writeln(x*x*x % (1-x) > 0); // -1000 % 11 = 1 > 0 : true
}

こんな感じになります。詳しくはこちら:
http://dlang.org/phobos/std_typecons.html#Proxy
http://dlang.org/operatoroverloading.html

majiangmajiang2012/04/20 09:20ありがとうございます。試してみます!

majiangmajiang2012/04/21 01:46試してみました。ご提示されたコードの除算は少し私の意図と異なるので変更しました。それから任意の (実際には符号ありのみで十分) 整数型を拡張できるように変更しようと思っていじっていますが、どうすればよいでしょうか。

まず proper_div, proper_mod という名前で正しい挙動を定義し、任意の型で動くように変更することはできました。(型パラメータTに演算が入っていない場合は実行時エラーになるのでしょうか? コンパイルエラー出してくれるのですか?)

auto proper_div(T)(T x, T y)
{
T q = x / y;
if ((0 < y && x < q*y) || (y < 0 && q*y < x))
{
q -= 1;
}
return q;
}

auto proper_mod(T)(T x, T y)
{
return x - proper_div(x, y)*y;
}

上の方法で int を拡張したものを pint としていますが、proper(int) のような記法で任意の型へ変更を加えることはできるでしょうか。

また、少し話題が変わりますが、数値型のうち cent だけエラーになってしまいます。128ビット整数を使うには特別な操作が必要ですか?

質問ばかりですみません。

majiangmajiang2012/04/21 02:01現在のコード: https://gist.github.com/2430277/7710792eb33809b3289943a9db461af53179b629

cafeliercafelier2012/04/21 09:21> (型パラメータTに演算が入っていない場合は実行時エラーになるのでしょうか? コンパイルエラー出してくれるのですか?)

コンパイルエラーです。もうお試しになられたかとも思いますが、proper_div("string", "mojiretsu"); などとするとコンパイル時にひっかかります。

> proper(int) のような記法

できます。gistのコードでほぼOKです。class proper(T) ではなく struct proper(T) にするとうまく行くと思います。

D言語で解説した記事がとっさに思い出せなかったのでC#の記事ですが、classが参照型でstructが値型という違いがあります。http://ufcpp.net/study/csharp/oo_reference.html
class にする場合は proper!long(4) ではなく new proper!long(4) で値を作ることになります。

> 数値型のうち cent だけエラーになってしまいます。128ビット整数を使うには特別な操作が必要ですか?

残念なことに、centはキーワードだけ予約されていて、実装はまだされていません(^^;。現状、組み込みの整数型ではlongの64ビットが最大で、それ以上はstd.bigint.BigIntを使うことになります。

bicycle1885bicycle18852013/11/15 11:08素数のカウントのプログラムですが、
> bool[] is_prime = new bool[N];
がfalseで初期化されるので、その直後に
> is_prime[] = true;
などとtrueを突っ込む必要があると思います。

トラックバック - https://topcoder-g-hatena-ne-jp.jag-icpc.org/cafelier/20120416
 | 

presented by cafelier/k.inaba under CC0