2012年6月30日土曜日

TopCoder SRM402 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

words[]という1つの要素が1つの単語からなる文字列型配列が与えられる。i番目の要素がi番目の単語を省略したものを返せ。単語の省略したものというのは、他の単語の接頭辞にならない、かつ空文字ではないという条件を満たす、もっとも短い接頭辞である。この制約により、すべての与えられた単語の省略した形があるということが保証される。

私の解答はこちら。

public class WordAbbreviation {

 public String[] getAbbreviations(String[] words) {
  String[] ret = new String[words.length];
  StringBuffer sb = new StringBuffer();
  for( int i=0 ; i<words.length ; i++ ){ // 各単語について
   for( int j=0 ; j<words[i].length() ; j++ ){ // 先頭からj番目の文字まででユニークか判定
    sb.append(words[i].charAt(j)); // 1文字増やす
    boolean isUnique = true;
    String tmp = sb.toString();
    for( int k=0 ; k<words.length ; k++ ){
     if( i==k ) continue;
     if( words[k].startsWith(tmp) ){ // 接頭辞で単語の特定ができない場合
      isUnique = false;
      break;
     }
    }
    if( isUnique ){
     ret[i] = tmp;
     break;
    }
   }
   sb.delete(0, sb.length());
  }
  return ret;
 }

}

得点は225.45/250、1回のsubmitでシステムテストクリア。中央値は約158点。

2012年6月28日木曜日

TopCoder SRM401 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

ジョンはフィールドテックという会社で働いている。そして今日、ジョンは仕事後にとても疲れていたので、家に帰るとすぐに眠ってしまった。不幸なことに、寝ている時でさえもジョンは仕事のことが忘れられなかった。夢の中で、ジョンは人参栽培を行っている会社から以下の問題に対処するように頼まれた。2つの人参を結ぶ直線状に何本の人参が育てられるだろうか?ただし端点の人参は除くものとして考える。これはかなり変な問題であるが、さらに問題を変なものにしていることとして、会社の代表はすべての人参は無限の広さを持つ平面で育ち、1つの格子点(訳注:座標が整数の箇所のこと)に1つの人参だけ育てられるということを述べた。あなたはこの問題に対処する疲れ切ったジョンを助けなければならない。

2本の端点の人参の座標を(x1, y1), (x2, y2)としたときに、これらの位置を結ぶ線分上で育てられる人参の数を返すメソッドを作成せよ。

public class DreamingAboutCarrots {

 public int carrotsBetweenCarrots(int x1, int y1, int x2, int y2) {
  int num = Math.abs(y2 - y1);
  int denom = Math.abs(x2 - x1);
  int step = gcd(num, denom);
  return step-1;
 }
 private static int gcd(int a, int b){
     return b == 0 ? a : gcd(b, a % b);
 }
}

得点は191.09/250、1回のsubmitでシステムテストクリア。中央値は約142点。結局x、y方向の座標の差を計算し、その値の最大公約数-1が答えになる。短いコードでかける気持ちのいい問題。当日の正解率は34%台と難しめ。

2012年6月26日火曜日

初めてのコンピュータサイエンス解答まとめ

章ごとに解答の記事があるので、以下にリンクでまとめました。1章、13章は問題がないので、解答はありません。英語版テキストはこちらをご覧ください。日本語版のpdfはありませんので、必要があれば購入しましょう。なお、英語版と日本語版では一部の問題が異なっていますのでご注意ください。

2012年6月24日日曜日

TopCoder SRM400 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

あなたは休日に街の中を歩いている。すると突然、上司から携帯電話でできるだけ速く職場に来いという命令が届いた。街の大きさは無限であり、南北方向に道路がXの間隔で並んでおり、東西方向に道路がYの間隔で並んでいると仮定する。いま、あなたは(0, 0)という座標におり、職場は(gX, gY)という位置にある。街にはタクシーが数台あり、(tXs[i], tYs[i])という位置にいる(訳注:配列になっているのは複数台あることを意味する)。職場へは徒歩、あるいは留まっているタクシーへ徒歩で向かい、タクシーで職場へ行くという方法がある。東西方向、あるいは南北方向の隣合う交差点へ向かうのにwalkTime分、タクシーが同様の移動をするのにtaxiTime分かかるとする。このとき、職場へ向かうのに最低限必要となる時間を求めて返すメソッドを作成せよ。

私の解答はこちら。

public class GrabbingTaxi {

 public int minTime(int[] tXs, int[] tYs, int gX, int gY, int walkTime, int taxiTime) {
  // 最初の基準は徒歩のみでかかる移動時間
  int min = (Math.abs(gX) + Math.abs(gY)) * walkTime;
  for( int i=0 ; i<tXs.length ; i++ ){
   // タクシーiを利用した場合について検討する
   // タクシーiでの移動時間
   int taxi_time = (Math.abs(gX - tXs[i]) + Math.abs(gY - tYs[i])) * taxiTime;
   // タクシーiの位置まで徒歩で移動するのにかかる時間
   int walk_time = (Math.abs(tXs[i]) + Math.abs(tYs[i])) * walkTime;
   min = Math.min(min, taxi_time + walk_time); // 最小値を更新したか検討
  }
  return min;
 }

}

得点は239.07/250、1回のsubmitでシステムテストクリア。中央値は約179点。

2012年6月22日金曜日

初めてのコンピュータサイエンス第15章解答

いよいよ最終章、第15章練習問題の解答です。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず(英語版はpdfで公開されている模様)。日本語版が必要なら買いましょう。その前にこの章の個人的なメモ。

  • Pythonの標準ライブラリにはSQLite3というSQLiteというDBを操作するためのライブラリがある。SQLiteは大規模なものには向かないが、小規模なものであれば十分利用に耐えうる。
  • データベースにプログラムが直接手を加えるということはなく、DBMSが操作の仲介の役割を果たす。
  • データベースのキーにID(つまり整数)を持たせることには重要な意味がある。整数の方がソートが高速であるということ、同じ名前の物、人物の区別に便利だからである。
  • NULLの取り扱いはデータベースによって異なる。データベースはSQLite以外にもMySQL、OrableDB、Accessなどがある。
  • クエリーはネストできる。特定の条件Aに合うものだけ抽出し、その抽出したものに対し、別の条件Bに合うものだけを抽出するといったことができる。
SQLの基本的な決まり文句の一覧には以下のようなものがある。
コマンド 意味
CREATE TABLE Men(Name TEXT, Age INTEGER)Name(人物名)という文字列型の列、Age(年齢)という整数型の列からなるMenというデータベースを作成する
INSERT INTO Men VALUES("Taro", 30)MenというDBに("Taro", 30)という値のレコードを挿入する
SELECT Name FROM Men WHERE Age >= 25年齢が25歳以上の人物の名前を抽出する
SELECT Name FROM Men WHERE Age >= 25 AND Age <=40年齢が25歳以上40歳以下の人物の名前を抽出する
SELECT * FROM Men WHERE Name="Taro"名前が"Taro"という人をすべて抽出する
UPDATE Men SET Age=31 WHERE Name="Taro"TaroのAgeを31に更新する
DELETE FROM Men WHERE Name="Taro"NameがTaroというレコードを削除する。TaroというNameのものがすべて消える
CREATE TABLE Men(Name TEXT NOT NULL, Age INTEGER)NameがNULLのレコードを認めないデータベースを作成する。
INNER JOIN直積を作成する(クエリー間で比較するときに使える)
PRIMARY KEY一意になるキー列を設定する(キーを複数入れないようにするエラーチェックに使える)
SELECT AVG(Age) FROM Men平均年齢を計算する
SELECT MAX(Age) FROM Men GROUP BY NameNameごとに年齢の最大値を計算する
SELECT DISTINCT Name FROM MenNameの重複を排除してNameを抽出

では解答に移ることにする。

1

a)~c)の解答。DBへのデータの挿入はFor文とタプルを利用した方法でもよいでしょう。

# -*- encoding: utf8 -*-
import sqlite3 as dbapi

con = dbapi.connect('census.db')
cur = con.cursor()

cur.execute('CREATE TABLE Density(State TEXT, Population INTEGER, Area REAL)')

cur.execute('INSERT INTO Density VALUES("Newfoundland and Labrador", 512930, 370501.69)')
cur.execute('INSERT INTO Density VALUES("Prince Edward Island", 135294, 5684.39)')
cur.execute('INSERT INTO Density VALUES("Nova Scotia", 908007, 52917.43)')
cur.execute('INSERT INTO Density VALUES("New Brunswick", 729498, 71355.67)')
cur.execute('INSERT INTO Density VALUES("Quebec", 7237479, 1357743.08)')
cur.execute('INSERT INTO Density VALUES("Ontario", 11410046, 907655.59)')
cur.execute('INSERT INTO Density VALUES("Manitoba", 1119583, 551937.87)')
cur.execute('INSERT INTO Density VALUES("Saskatchewan", 978933, 586561.35)')
cur.execute('INSERT INTO Density VALUES("Alberta", 2974807, 639987.12)')
cur.execute('INSERT INTO Density VALUES("British Columbia", 3907738, 926492.48)')
cur.execute('INSERT INTO Density VALUES("Yukon Territory", 28674, 474706.97)')
cur.execute('INSERT INTO Density VALUES("Northwest Territories", 37360, 1141108.37)')
cur.execute('INSERT INTO Density VALUES("Nunavut", 26745, 1925460.18)')

con.commit()

d)~j)の解答はこちら。基本的なSQL文の練習問題です。j)の計算結果を列として並べて表示するという方法は調べて納得。

# -*- encoding: utf8 -*-

import sqlite3 as dbapi

con = dbapi.connect('census.db')
cur = con.cursor()

con.text_factory = str

# d)の解答
cur.execute('SELECT * FROM Density')
ans_d = cur.fetchall()
print ans_d

# e)の解答
cur.execute('SELECT Population FROM Density')
ans_e = cur.fetchall()
print ans_e # リストの中身は要素1のタプルである

# f)の解答
cur.execute('SELECT State FROM Density WHERE Population < 1000000')
ans_f = cur.fetchall()
print ans_f

# g)の解答
cur.execute('SELECT State FROM Density WHERE \
             Population < 1000000 OR Population > 5000000')
ans_g = cur.fetchall()
print ans_g

# h)の解答
cur.execute('SELECT State FROM Density WHERE \
             NOT(Population < 1000000 OR Population > 5000000)')
ans_h = cur.fetchall()
print ans_h

# i)の解答
cur.execute('SELECT Population FROM Density WHERE Area > 200000')
ans_i = cur.fetchall()
print ans_i

# j)の解答 州の名前と人口密度を表示
cur.execute('SELECT State, Population / Area FROM Density')
ans_j = cur.fetchall()
print ans_j

2

1のDBも使って解答します。まずは次のコードで、データを挿入します。

# -*- encoding: utf8 -*-

import sqlite3 as dbapi

con = dbapi.connect('census.db')
cur = con.cursor()

con.text_factory = str

cur.execute('CREATE TABLE Capitals(State TEXT, Capital TEXT, Population INTEGER)')

# Province/Territory Capital Population
data =(
("Newfoundland and Labrador", "St. John’s", 172918),
("Prince Edward Island", "Charlottetown", 58358),
("Nova Scotia", "Halifax", 359183),
("New Brunswick", "Fredericton", 81346),
("Quebec", "Quebec", 682757),
("Ontario", "Toronto", 4682897),
("Manitoba", "Winnipeg", 671274),
("Saskatchewan", "Regina", 192800),
("Alberta", "Edmonton", 937845),
("British Columbia", "Victoria", 311902),
("Yukon Territory", "Whitehorse", 21405),
("Northwest Territories", "Yellowknife", 16541),
("Nunavut", "Iqaluit", 5236)
)

for d in data:
    cur.execute('INSERT INTO Capitals VALUES(?, ?, ?)', (d[0], d[1], d[2]))

con.commit()

以下、a)~i)までの解答です。

# -*- encoding: utf8 -*-

import sqlite3 as dbapi

con = dbapi.connect('census.db')
cur = con.cursor()

con.text_factory = str

cur.execute('SELECT * FROM Capitals')
ans_a = cur.fetchall()
print ans_a

cur.execute('SELECT Density.Population, Capitals.Population \
FROM Density INNER JOIN Capitals WHERE Density.State = Capitals.State')
ans_b = cur.fetchall()
print ans_b

cur.execute('SELECT Density.Area FROM Density INNER JOIN Capitals \
WHERE Capitals.Population > 100000 AND Density.State = Capitals.State')
ans_c = cur.fetchall()
print ans_c

cur.execute('SELECT Density.Area FROM Density INNER JOIN Capitals \
WHERE Capitals.Population > 500000 AND \
Density.Population *1.0 / Density.Area < 2.0 AND \
Density.State = Capitals.State')
ans_d = cur.fetchall()
print ans_d

cur.execute('SELECT SUM(Area) FROM Density')
ans_e = cur.fetchall()
print ans_e

cur.execute('SELECT AVG(Population) FROM Capitals')
ans_f = cur.fetchall()
print ans_f

cur.execute('SELECT MIN(Capitals.Population) FROM Capitals')
ans_g = cur.fetchall()
print ans_g

# 著者による解答は間違ってそうな気がします。著者はもっとも多い人口の値を求めていますが、
# 求める必要があるのは、人口最大の州の名前です。
cur.execute('SELECT Density.State FROM Density \
WHERE Density.Population IN \
    (SELECT MAX(Population) FROM Density)')
ans_h = cur.fetchall()
print ans_h

cur.execute('SELECT A.State, B.State FROM \
Density A INNER JOIN Density B WHERE \
ABS(A.Population * 1.0 / A.Area - B.Population * 1.0 / B.Area) < 0.5 \
AND A.State < B.State')
ans_i = cur.fetchall()
print ans_i

3

SELECT文の結果を表示すると、空のリストになる。Pythonで1/0を含めるとZeroDivisionErrorになる。

>>> 1/0
Traceback (most recent call last):
  File "<interactive input>"line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
>>> 1 > 0 and 1 / 0
Traceback (most recent call last):
  File "<interactive input>"line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
>>> 1 / 0 and 1 > 0
Traceback (most recent call last):
  File "<interactive input>"line 1, in <module>
ZeroDivisionError: integer division or modulo by zero

4

デフォルト引数を利用することで、外部からパラメータを渡すことができるようになる。もっとチェックするのであれば、query中の?の数とタプルの要素数が一致していることを確認するということが挙げられます。問題文の趣旨から外れていきそうなので、そこまではしませんが。

# -*- encoding: utf8 -*-

def run_query(db, query, param=None):
    '''データベースdbに対してqueryを実行して結果を返す '''

    con = sqlite.connect()
    cur = con.cursor()
    if param != None:
        cur.execute(query)
    else:
        if type(param) == tuple: # タプルのみを受け付ける
            cur.execute(query, param)
    data = cur.fetchall()
    cur.close()
    con.close()
    return data

# example
run_query(db, 'SELECT * FROM Precipitation')
run_query(db, 'SELECT City, Temp FROM Precipitation \
    WHERE Snow >= (?) and Temp > (?)', (s, t))

2012年6月21日木曜日

TopCoder SRM399 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

0からn-1まで番号をつけられた、環状の地下鉄の路線がある。駅0を出て、駅1に到着するのにt[0]という時間がかかる。同様に、駅1を出て駅2に到着するのにt[1]という時間がかかるし、駅n-1を出て駅0に到着するのにはt[n-1]という時間がかかる。駅と駅の間はどちらかの方向に向かってくことができる(訳注:時計回りと反時計回りをイメージしよう)。つまり、ある駅からある駅へ向かうのに、同じ駅を2回以上通らないで向かう方法は2通りあることになる。例えば4つ駅があるものとし、1から3へ向かうのであれば、1-2-3、あるいは1-0-3という向かい方があることになる。前者では移動時間の合計はt[1]+t[2]であり、後者はt[0]+t[3]になる。ある人が別の駅へ向かいたいと思っているとき、その人はいつも速く到着できる方を選ぶ。さて、t[]という移動時間の一覧が与えられたとき、2駅間の移動でどんなに速く動いても最も時間がかかってしまう駅のペアを求め、その移動にかかる時間を返すメソッドを作成せよ。

私の解答はこちら。

public class CircularLine {

 public int longestTravel(int[] t) {
  int[][] dist = new int[t.length][t.length];
  int[] tt = new int[t.length*2];
  for( int i=0 ; i<tt.length ; i++ ){
   tt[i] = t[i % t.length]; // tを2回繰り返したもの
  }
  for( int i=0 ; i<t.length ; i++ ){
   for( int j=0 ; j<t.length ; j++ ){
    // iからjへ向かう距離を計算
    int from = i;
    int to = j;
    if( j<i ){
     from = i;
     to = j + t.length;
    }
    for( int k=from ; k<to ; k++ ){
     dist[i][j] += tt[k];
    }
   }
  }

  int max = 0; // 2駅間の移動でもっともかかる時間
  for( int i=0 ; i<t.length ; i++ ){
   for( int j=i+1 ; j<t.length ; j++ ){
    int minDist = Math.min(dist[i][j], dist[j][i]); // 駅i~j間で最速の移動をしても
    max = Math.max(max, minDist); // 移動にかかる時間の最大値を更新してしまうか?を検討
   }
  }
  return max;
 }

}

得点は147.55/250、1回のsubmitでシステムテストクリア。中央値は約156点。2駅の移動は2次元配列で管理しています。dist[i][j]、dist[j][i]が駅iから駅jへ向かうという意味であり、それぞれ添え字の増える方向に向かった場合、減った方向に向かった場合の2種類の経路でかかる時間になります。したがってdist[i][j]とdist[j][i]のうち、小さい方がある人が選択するルートになります。添字操作が面倒なので、tを2回繰り返すことで、添え字が小さくなる方向への探索を簡単に置き換えることにしています。

2012年6月20日水曜日

TopCoder SRM398 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

A0、X、Y、Mとnという整数が与えられる。以下のルールに従って、数列Aを作成する。

  • A[0] = A0
  • A[i] = (A[i - 1] * X + Y) % M (0 < i < n)
できあがった数列Aの、2要素の絶対値誤差が最小になる組を求め、その絶対値誤差を返すメソッドを作成せよ。

私の解答はこちら。

import java.util.Arrays;

public class MinDifference {

 public int closestElements(int A0, int X, int Y, int M, int n) {
  int[] A = new int[n];
  A[0] = A0;
  for( int i=1 ; i<n ; i++ ){
   A[i] = (A[i-1] * X + Y) % M;
  }
  Arrays.sort(A);
  int min = Math.abs(A[1] - A[0]);
  for( int i=1 ; i<n ; i++ ){
   min = Math.min(min, Math.abs(A[i] - A[i-1]));
  }
  return min;
 }

}

得点は246.56/250、1回のsubmitでシステムテストクリア。中央値は約217点。工夫したのは数列Aに対し、ソートするという点で、これにより、絶対値誤差の最小値の候補が隣合う要素のみになるということが挙げられます。

2012年6月18日月曜日

初めてのコンピュータサイエンス第14章解答

第14章練習問題の前半の解答です。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず(英語版はpdfで公開されている模様)。日本語版が必要なら買いましょう。

1

このコードは実行すると、ボタンが出てきます。ボタンを押すとアプリケーションが終了するようになっています。

import Tkinter as tk

class Goodbye:
    def __init__(self, parent):
        self.parent = parent
        self.frame = tk.Frame()
        self.frame.pack()

        self.bye = tk.Button(self.frame, text='Good-bye', command=self.byebye)
        self.bye.pack()

    def byebye(self):
        self.parent.destroy()


if __name__ == '__main__':
    window = tk.Tk()
    myapp = Goodbye(window)
    window.mainloop()

2

このコードを実行すると数字が入ったボタンが出てきます。ボタンをクリックすると、表示される値が1ずつ大きくなります。

import Tkinter as tk

class CountUp:
    def __init__(self, parent):
        self.parent = parent
        self.frame = tk.Frame(parent)
        self.frame.pack()

        self.state = tk.IntVar()
        self.state.set(0)

        self.button = tk.Button(parent, text=self.state.get(),
                                command=self.count)
        self.button.pack()

    def count(self):
        self.state.set(self.state.get() + 1)
        self.button.config(text=self.state.get())

if __name__ == '__main__':
    window = tk.Tk()
    app = CountUp(window)
    window.mainloop()

3

x = lambda(): 3を読みやすくせよということなのですが、このコードを実行するとエラーになります。読みやすくする以前の問題で、何がしたいのでしょうか?

4

# -*- encoding: utf8 -*-

import Tkinter as tk

class CountDNA:
    def __init__(self, parent):
        self.parent = parent
        self.frame = tk.Frame()
        self.frame.pack()

        # モデルはATGCの数
        self.dic = {'A' : 0, 'T' : 0, 'G' : 0, 'C' : 0}
        self.message = tk.StringVar()

        # DNAの文字列を表示する
        self.textbox = tk.Text(self.frame, width=30, height=10)
        self.textbox.pack()

        # 数えるボタンを表示する
        self.bt = tk.Button(self.frame, text="数える",
                  command=lambda: self.countATGC(self.dic, self.message))
        self.bt.pack()

        # 数えた結果を表示する
        self.label = tk.Label(self.frame, textvariable=self.message)
        self.label.pack()


    # 出現回数を計算する
    def countATGC(self, dic, msg):
        for (key, value) in dic.iteritems():
            dic[key] = 0

        for c in self.textbox.get('0.0', tk.END):
            if c in dic.keys():
                dic[c] = dic.get(c, 0) + 1
        # print dic
        msg.set(self.printATGC(dic))

    def printATGC(self, dic):
        msg =  u'Aの数:%d Tの数:%d Cの数:%d Gの数:%d' % \
               (dic['A'], dic['T'], dic['C'], dic['G'])
        return msg

if __name__ == '__main__':
    window = tk.Tk()
    app = CountDNA(window)
    window.mainloop()

5

練習ということで、敢えてクラスを使わないで書いてみました。ちなみにこのコードをとあるWebエンジニアに見せたところ、__init__に何もかも押し込むのは、Cで言うところのmainに1000行書くようなものだということで、あまり良い作法でないということを教わりました。機能自体は実現できましたが、更によい書き方を知る必要もありそうです。

# -*- encoding: utf8 -*-

import Tkinter as tk

window = tk.Tk()
frame = tk.Frame(window)
frame.pack()
label = tk.Label(frame, text="華氏表現の温度")
label.pack()

var = tk.DoubleVar()

entry = tk.Entry(frame, textvariable=var) # 数値入力部
entry.pack()

svar = tk.StringVar()

msg = tk.Label(frame, textvariable=svar)
msg.pack()

bt_trans = tk.Button(frame, text="変換", command=lambda: click_trans(var,svar))
bt_trans.pack()

bt_end = tk.Button(frame, text="終了", command=lambda: click_end(window))
bt_end.pack()

def to_celsius(t):
    return (t - 32.0) * 5.0 / 9.0

def click_trans(var,svar):
    try:
        # var.get()が文字列だと例外が発生する
        s_temp = "%.10f" % to_celsius(var.get())
        svar.set(s_temp)
    except:
        svar.set("???")

def click_end(window):
    window.destroy()

window.mainloop()

2012年6月14日木曜日

TopCoder SRM397 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

あなたは敵の暗号を解読しなければならない秘密の任務を負った。既に敵が以下の方法によってメッセージを暗号化しているということをつかんでいる。

  • 各文字は'a'から'z'の間の値だけからなり、アルファベットには01~26までの二桁の数字が割り当てられている。メッセージは文字を割り当てられた数字で単に置き換えただけのものである。
  • 例えば、tが20、eが05、sが19という数字を割り当てられていたとすれば、"test"という文字列は"20051920"と暗号化される。

codeという数字を文字に対応させたものが与えられる。codeの先頭の文字は01、2番目の文字は02などのように数字が割り当てられている。また、messageという暗号化後あるいは暗号化前の文字列が与えられる。暗号化前であれば暗号化したメッセージを、暗号化後の文字列が与えられているなら暗号化前の元のメッセージを返すようなメソッドを作成せよ。

私の解答はこちら。

public class BreakingTheCode {

 public String decodingEncoding(String code, String message) {
  StringBuilder sb = new StringBuilder();
  // 先頭の文字を見て処理のパターンを変えている
  if( message.charAt(0) >= '0' && message.charAt(0) <= '9'){ 
   for( int i=0 ; i<message.length()/2 ; i++ ){
    // 数字は2桁なので2文字取り出して整数にしておく
    int num = Integer.parseInt(message.substring(i*2, (i+1)*2));
    sb.append(code.charAt(num-1));
   }
  }else{
   for( int i=0 ; i<message.length() ; i++ ){
    char c = message.charAt(i);
    int num = code.indexOf(c) + 1; // 先頭は01から始まることに注意
    String snum = String.format("%02d", num);
    sb.append(snum);
   }
  }
  return sb.toString();
 }

}

得点は231.19/250、1回のsubmitでシステムテストクリア。先頭の文字が数値か否かを判別するには、asciiで'0'以上'9'以下とするよりも、Character.isDigit(0)を使うと良いでしょう。

2012年6月12日火曜日

初めてのコンピュータサイエンス第12章解答(後半)

第12章練習問題の後半10問の解答です。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず(英語版はpdfで公開されている模様)。日本語版が必要なら買いましょう。

11

one_item_listというリストが2回使われているが、1回目の利用でリストの内容を取り除いているので、2回目の利用時に要素数が空になっているため、assert len(one_item_list)==1で失敗してしまう。これを避けるためには、リストのコピーなどしておけばよいであろう。

12

文字列をファイルのように扱うことができる点においてテストを簡単にできる(テキストファイルをいちいち用意しなくても良いから)。詳しくはオンラインドキュメントのこちらを参照してほしい。

def count_lines(f):
    nLine = 0
    for line in f:
        nLine += 1
    return nLine
import cStringIO
import nose
from file_util import count_lines

def test_str_to_file():
    data = 'this\nis\na\npen'
    s = cStringIO.StringIO(data)
    assert count_lines(s) == 4

if __name__ == '__main__':
    nose.runmodule()

13

毎回各要素がこれまでに探索した要素の最大値より大きいか、あるいは最小値より小さいかを調べて更新している。最初にmax = None、min = Noneとしているが、不等式で評価する際、Noneは0扱いされている。そのため、すべて正の値、あるいはすべて負の値からなるリストが入力されるとおかしな挙動になる。従って、次のように書き換える。

def find_min_max(values):
    min = None
    max = None

    if len(values):
        min = values[0]
        max = values[0]
        for value in values:
            if value > max:
                max = value
            if value < min:
                min = value

    print 'min = %s' % min
    print 'max = %s' % max

if __name__ == '__main__':
    find_min_max([])
    find_min_max([6])
    find_min_max([-1, 1, 3, 5])

14

a)、b)の解答は、limitに負の値を与えたとき、終了条件のcurrent==0が満たされず、無限ループになるということである。

c)の解答としては、current != 0でループを続けるのではなく、current > 0でループを続けるようにすればよい。正の数を入力すればいずれ0になって止まるし、0以下であれば、ループに入らず終了する。

15

valueが0の時に0で割るため、例外が発生する。0で割るとZeroDivisionErrorが発生するので、これを捕捉し、Noneを返すことにする。ここでは、問題文中のコードのコメントに従い、値が逆数を持たない場合はリストにNoneを入れるというルールに従う。

def compute_reciprocals(values):
    reciprocals = []
    for value in values:
        try:
            reciprocals.append(1/value)
        except ZeroDivisionError:
            reciprocals.append(None)
    return reciprocals

val = [-1.0, 0, 1.0, 2.0, 3.0]
print compute_reciprocals(val) # [-1.0, None, 1.0, 0.5, 0.3333333333333333]

16

find_last関数について。v0は行数をカウントしている。v1は文字列の含まれる最終行の行番号と、その行に含まれる内容になる。v2はファイル、v3はファイルの各行を収める変数である。変数名を書き換えるのであれば、v0=num_line、v1=last_line、v2=filename、v3=lineなどが考えられる。

一方、standard_deviation関数について。v0はリストに含まれる数値の合計、v1はvaluesの各要素を格納する一時的な変数を表している。v2はリストの要素から計算される平均、v3は分散、v5は標本分散の平方根、すなわち標準偏差になる。変数名を書き換えるのであれば、v0=total、v1=val、v2=mean、v3=var、v4=val(v1と同じリストを使うループなのでv1に合わせた)、v5=sd(standard deviationの略語)などが考えられる。

17

まさかもう一度中央値、平均の実装を行うことになるとは...最頻値の導出については、C言語だったらさぞ面倒だろうなと思った次第。

# -*- encoding: utf8 -*-

def mean(values):
    total = 0 # 要素の合計を格納
    for val in values:
        if not type(val) in (int, float):
            raise ValueError
        total += val

    # floatにキャストするのはvaluesが
    # 全部整数だったときに切り捨てが発生するから
    return float(total)/len(values)

def median(values):
    for val in values:
        if not type(val) in (int, float):
            raise ValueError

    L = sorted(values[:]) # valuesの書き換えはしないことにする
    length = len(L)
    if length % 2 == 0:
        return (L[length/2-1]+L[length/2]) / 2.0
    else:
        return L[length/2]

def mode(values):
    for val in values:
        # 1/3のような実数は辞書のキーにしないものとする
        # 細かい丸め誤差があるときに同一か判別するのは厄介なので
        if type(val) != int:
            raise ValueError

    dic = {} # 整数の出現回数をカウント
    for val in values:
        dic[val] = dic.get(val, 0) + 1
    max_val = max(dic.values()) # 出現回数の最大値
    ret = []
    for key in dic.keys():
        if dic[key] == max_val: # 出現回数の最大値に一致するキーのみ抽出
            ret.append(key)
    return ret

if __name__ == '__main__':
    print mean([1, 3, 5, 7]) # 4.0
    try:
        print mean([1, 'a', 6])
    except ValueError:
        print "ValueError was caught in mean!"
    print median(range(10)) # 0~9なので4.5
    print median(range(10)) # 0~10なので5
    try:
        print median([1, 'a', 6])
    except ValueError:
        print "ValueError was caught in median!"

    print "key =", mode([0, 1, 0, 3, 2, 4, 1])
    try:
        print mode([1, 3.14, 6])
    except ValueError:
        print "ValueError was caught in mode!"

18

def process_file(filename):
    try:
        f = open(filename, 'r')
        for line in f:
            line = line.strip()
            print line
    except:
        pass

    f.close()

19

問題文には「アサーションを追加し、フォーマットに誤りがあればエラーメッセージを出力して終了するようにしてください」とあるのですが、アサーションというのはテスト、デバッグのために使うものであり、リリースする製品には含めてはいけないようなコードである。従って「アサーションを追加」するのではなく、「エラー処理を追加」するのが正しいはず。エラーが発生するとすれば、splitとその周辺の処理に限られるはずなので、そこにtryをかけて例外を補足すればよい。

# -*- encoding: utf8 -*-

def read_molecule(r):
    line = r.readline()
    if not line:
        return None

    try:
        key, name = line.split()
    except: # 例えばsplitの結果、引数が3つになるとここにくる
        print "Error in COMPND Name"
        return None # 例外発生時の返り値は要検討だが、Noneとする

    molecule = [name]
    reading = True

    while reading:
        line = r.readline()
        if line.startswith('END'):
            reading = False
        else:
            try:
                key, num, type, x, y, z = line.split()
                molecule.append((type, x, y, z))
            except: # ENDがない、あるいはsplitの結果が6つでないなどの原因で例外が発生する
                print "Error in reading"
                return None

    return molecule

20

a)~d)の解答があるのだが、まとめて解答する。a)、b)はdocstringが解答になる。c)はテストケースの作成、d)はテストケースに通る関数の作成である。

# -*- encoding: utf8 -*-

import nose

def findButNotAfter(source, prefix, pattern):
    '''sourceの中で最初にpatternが現れた位置(ただし直前がprefixに
    なってはなりません)の添え字を返す。例えば、
    findButNotAfter('abcdcd', 'ab', 'cd')は4を返す。最初のcdは
    直前にabがあるので不適切。以下、仕様があいまいな箇所を修正
    1)
    findButNotAfter('abcdcd', 'bc', 'cd')とした場合、2を返すものとする。
    1回目のcdの前にはabしかないと考える(abcを探索範囲としない)。
    2)
    pattern、source、prefixのいずれかが空、あるいはNoneの場合、-1を返す。
    3)
    patternがsourceにない場合、例えば
    findButNotAfter('abcdcd', 'bc', 'de')は-2を返す。
    4)
    patternはあるが、prefixが常に前にある場合は-3を返す
    '''

    if not pattern or not source or not prefix:
        return -1
    pos = source.find(pattern, 0)
    if pos < 0:
        return -2
    while pos >= 0:
        if not source[:pos].endswith(prefix): # 1番目の仕様はここに効く
            return pos
        pos = source.find(pattern, pos+1)

    return -3 # 常にpatternの直前にprefixがある場合

def test_pattern_is_null():
    ''' patternが空 '''
    assert findButNotAfter('abcdcd', 'bd', '') == -1

def test_no_prefix():
    ''' prefixが空の場合 '''
    assert findButNotAfter('abcdcd', '', 'cd') == -1

def test_no_source():
    ''' sourceが空の場合 '''
    assert findButNotAfter('', 'cd', 'ab') == -1

def test_no_pattern_in_source():
    ''' patternがsourceの中にない '''
    assert findButNotAfter('abcdcd', 'bd', 'ef') == -2

def test_theres_always_prefix_previously():
    ''' いつもpatternの直前にprefixがある場合 '''
    assert findButNotAfter('abcdabcd', 'ab', 'cd') == -3

def test_prefix_pattern_is_the_same():
    ''' prefixとpatternが一致する場合 '''
    assert findButNotAfter('abcdefg', 'd', 'd') == 3

def test_all_the_same_chars():
    ''' 全部文字が同じ場合 '''
    assert findButNotAfter('aaaaaaaaaaa', 'aa', 'aa') == 0

def test_match_at_first_char():
    ''' 先頭でマッチした場合 '''
    assert findButNotAfter('abcdcd', 'bc', 'abcd') == 0

def test_pattern_and_source_are_the_same():
    ''' patternとsourceが一致する場合 '''
    assert findButNotAfter('abcdcd', 'ab', 'abcdcd') == 0

def test_two_valid_pattern_pos():
    ''' 複数箇所条件にあう場合は先に出現した位置を返す '''
    assert findButNotAfter('abcdabcd', 'ef', 'c') == 2

if __name__ == '__main__':
    nose.runmodule()

2012年6月11日月曜日

TopCoder SRM396 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

オンライン決済とあるクレジットカード番号の受け付けに関するアプリケーションを作成している。カードの番号は長いので、入力時に間違えやすい。そこで、ユーザによって入力された数字が妥当か確認できるようなメソッドを作成したいと考えている。すべての受付可能なカード番号について、ルーンの公式というものがなりたっている。ルーンの公式とは以下のようなものである。

  1. カード番号を桁ごとに分ける。21378は2 1 3 7 8と分けられる。
  2. もし桁数が偶数であれば、先頭を1とした順番とし、奇数の位置にある桁を2倍する。そうでなければ偶数の位置にある桁を2倍する。上の例では桁数は5で奇数なので、偶数の位置にある数字を2倍することになる。2 1 3 7 8は2 2 3 14 8となる。なお偶数番目というのは2倍する前のものであることに注意(2倍して桁数が2桁になったことは考慮しなくて構わない)。
  3. 各桁の総和を計算する。2桁の数字は1桁ずつに分解して足し算する。これにより、2 2 3 14 8は2+2+3+1+4+8=20になる。
もし上の手続きで得られた数が10で割り切れるのであれば、数字は妥当、そうでなければ妥当でないとなる。クレジットカードの番号を表す文字列cardNumberが与えられたとき、妥当であれば"VALID"、そうでなければ"INVALID"を返すようなメソッドを作成せよ。
public class VerifyCreditCard {

 public String checkDigits(String cardNumber) {
  boolean isEven = cardNumber.length() % 2 == 1 ? true : false; // 元の数字の桁数
  int total = 0; // 3のステップで得られる数字
  for( int i=0 ; i<cardNumber.length() ; i++ ){
   int num = cardNumber.charAt(i) - '0';
   if( isEven && (i+1) % 2 == 0){ // 2倍する数値の条件にマッチしている
    num *= 2;
   }else if( !isEven && i % 2 == 0){
    num *= 2;
   }
   String tmp = "" + num;
   for( int j=0 ; j<tmp.length() ; j++ ){ // 各桁ごとに数値を足し算していく
    total += tmp.charAt(j) - '0';
   }
  }
  if( total % 10 == 0 ){
   return "VALID";
  }else{
   return "INVALID";
  }
 }

}

問題提出先のサーバ再起動で提出時間に5分強の遅延が発生したため、本来の得点は分からずじまい。得点は175.87/250ですが、再起動がなければ205点ぐらいでしょうか。1回のsubmitでシステムテストをクリアしています。

2012年6月10日日曜日

初めてのコンピュータサイエンス第12章解答(前半)

第12章練習問題の前半の解答です。本章は問題が20問とかなり多いので、可読性の向上を目的に10問ずつに分けて公開します。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず(英語版はpdfで公開されている模様)。日本語版が必要なら買いましょう。その前にこの章の個人的なメモ。

  • デフォルト引数も、可変長引数も仮引数の右に置かなければならない。可変長引数は1つだけ設定することが許されている。
  • 可変長引数リストを呼び出すと、実引数はタプルとして関数定義に渡される
  • 例外はtry~except構文で書く。tryで例外が発生しない場合、exceptの後ろにelseをつけておくと、その内容を実行することができる。
  • 例外を生成する場合はraise 例外名(メッセージ)という書き方をするとよい。
  • 低いところからスロー、高いところからキャッチする。つまり例外を投げられる可能性がある関数で逐一キャッチするのではなく、関数を呼んでいる側でまとめてキャッチするとよいということである。エラーの処理場所を減らすことで、エラーの処理方法を変えることが容易にできる。

テスト駆動開発(TDD)のメリット

  • テストを必ず書かされることになる
  • プログラマにとって全部のケースに通ればOKという目安が与えられる
  • 文章よりも仕様を正確に与えられる
  • テストコードを書いている最中に設計について考えることができる

では解答にうつります。

1

Pythonの名前つき引数が利用される場面は引数が大量にある場合である。順番を気にするよりも、引数名と値を与えるほうが楽だからである。逆に、本来の順序が分からなくなる恐れがあるので、名前を省略した書き方をした際にバグを引き起こすというリスクが発生する。

2

-1は有効な添字である。-1ではなくNoneを指定しておくことで、何も指定されていないということがはっきりする。

3

a)の解答はこちらが解答に近いと思います(4.7.3、4.7.4の項目)。

b)の解答

def test(var, *pargs, **kwargs):
    print var
    print pargs
    print kwargs

test(1, 2, 3, x=4, y=5, z=6)

上のコードを実行すると次のようになる。

1
(2, 3)
{'y': 5, 'x': 4, 'z': 6}

varには値が1つ収まり、pargsには名前のついていない可変長引数、kwargsには名前つきの可変長引数が当てはめられる。テキストにあるように、受け取った関数では、varは変数、pargsはタプル、kwargsは辞書になる。従って、上の例ではvar=1(1番左にある値を代入)、pargs=(2, 3)(名前がついておらず、かつまだ利用されていない実引数の集まり)、kwargs={'x':4, 'y':5, 'z':6}(名前つきの実引数の集まり)という結果になり、その値が表示される。引数に与える順序としては通常の引数、キーワード引数、名前なし可変長引数、名前つき可変長引数の順序で左から並べなければならない。引数は次の順序でマッチングを行う。

  • 通常の引数の位置によるマッチング
  • キーワード引数の名前によるマッチング
  • *を使った引数が定義されていれば、上の2つでマッチングできなかったキーワード引数でない引数をタプルにまとめる
  • **を使った引数が定義されていれば、上の3つでマッチングできなかったキーワード引数を辞書にまとめる
  • その他指定されていない引数がデフォルト値が設定されていれば、その値を代入する

c)の解答

HTMLのタグを扱うときなどにキーワード引数を受け取れるようにしておくと便利である。例えばHTMLのimgタグには大量のオプション(altやwidthなど)が付けられるが、これを逐一引数に与えるのは面倒である。そこで指定したものだけをつけたいというときに利用すればよい。

4

ここで言う線が線分なのか直線なのかで解答が変わるので、問題文が曖昧になっています。問題文より、エラー処理は考えないということなので、入力に数値が必ず入るものとして、文字列が入ってきたときは考えないものとします。また、本問と次の問題において、問題文に登場する「線」は直線や半直線ではなく、線分と仮定します。

  • [[1.0, 1.0], [1.5, 1.5]]と[[2.0, 2.0], [3.0, 3.0]]のように、線は重ならないものの、延ばせば一致するという場合のテスト。線分は重ならないのでNoneが返される。
  • [[2.0, 2.0], [3.0, 3.0]]と[[2.0, 3.0], [3.0, 2.0]]のように、線が他方の線の真ん中あたりで重なる場合、交点の座標[2.5, 2.5]が返される。一般的に想定される重なり方をテスト。
  • [[1.0, 1.0], [2.0, 2.0]]と[[1.5, 1.5], [3.0, 3.0]]のように、線が一部重なる場合、第一引数[[1.0, 1.0], [2.0, 2.0]]が返される。
  • [[-1.0, 0.0], [4.0, 0.0]]と[[0.0, 0.0], [0.0, 2.0]]のように、一方が他方を包含する場合のテスト。第一引数[[-1.0, 0.0], [4.0, 0.0]]が返される。
  • [[0.0, 0.0], [0.0, 2.0]]と[[0.0, 1.0], [2.0, 1.0]]のように、線が他方の線の端で重なる場合、交点の座標[0.0, 1.0]が返される。ぎりぎり重なるという状況のテスト。
  • [[1.0, 1.0], [2.0, 2.0]]と[[2.0, 2.0], [3.0, 3.0]]のように、線の端と端で重なる場合のテスト。交点の座標[2.0, 2.0]が返される。

整数を引数に取った場合、実数として処理されるかということもテストで考えていましたが、次の問題文を読む限り、エラー扱いする入力のようなので、効果的な6種類のテストに含めることはできない模様です。

5

エラー処理のコードは全体の8割ほど占めると言われているので、まずはエラーが正しく処理されているかを確認する必要がある。エラーが起きない基本的なロジックではバグはあまり発生しないということもエラーからチェックすべきということを後押しする。以下、エラーが正しく処理されていることを確認するために考える6種類のテストを列挙した。

  • [["a", 1.0], [2.0, 3.0]]のような、引数に文字列が含まれる場合にValueErrorが返されるか
  • [[1, 2.0], [3.0, 4.0]]のような、引数が浮動小数点数になっていない場合にValueErrorが返されるか
  • [[1.0, 2.0], [3.0, 4.0, 5.0]]のように、浮動小数点数の対になっていない場合にValueErrorが返されるか
  • [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]のように、浮動小数点数の対になっていない場合にValueErrorが返されるか
  • [[1.0, 1.0], [1.0, 1.0]]のような線を特定する2点が同じ点であった場合にGeometryError例外が返されるか
  • [[1, 1], [1, 1]]のような浮動小数点数でない値(整数でなく文字列でもOK)の対が一致した場合、ValueErrorの方が返されるか(例外の実装の順序を確認)

6

106通り。前提は元素記号が国際コードに関係のないということである。元素記号と国際コードを1回以上引数に与えるようにテストケースを作成するので、106の元素と26の言語という数の大きい方を選べばよい。

7

==とするのがよくない。丸め誤差が発生してしまうため、必ずしも==で値が同じであることを正確に判定できるわけではない。実数が等しいかそうでないかを判定する際には差の絶対値が十分小さいということで評価するべき。このコードではabs(actual-expected)<10**-15などにする。10**-15とするのは、浮動小数点の丸め誤差の最小単位が10**-16程度なので、それよりも若干大きめにとる必要があるためである。もしくはDecimalモジュールを利用した方法を取ることを考える必要がある。

8

  • Acids、Basesというものの存在を意識せずテストするべき。つまり、インタフェースである引数と返り値のみ既知として、内部仕様に依存したテストはやめるべき。関数の書き換えなどにより、動作しなくなる恐れがある(例えばAcidsという名前を書き換えられたら途端に動作しなくなる)。
  • リリースしようとしているコードをテストのためだけに書き換えることはよくない。書き換え前の本番用データに問題があった場合に発生しうるバグを考慮するべき。

9

上はex12_9.py、下はtestprefixes.pyという名前がついています。関数のテストは4種類書けということなのですが、これにあいまいな仕様を含むのか含まないのかが問題文からではわかりません。ひとまず、あいまいな仕様の箇所も固定してテストするということにしています。あいまいな仕様というのは、空文字が含まれた場合の対処と、文字列の途中にある空白の取り扱いになります。

# -*- encoding: sjis -*-

def all_prefixes(string):
    ''' 空文字が入ってきた場合の返り値に対する仕様がないこと、
        複数の語からなり、途中に空白がある場合の仕様がない点は問題である。
        今回は、空白も語の途中と扱うものと考えることにする。
    '''
    ret = set()
    for i in range(1,len(string)+1):
        tmp = string[:i]
        ret.add(tmp)
    return ret

if __name__ == '__main__':
    print all_prefixes('lead')
    print all_prefixes('')
    print all_prefixes('a pen')

テスト用コード

# -*- encoding: sjis -*-

import nose
from ex12_9 import all_prefixes

def test_null():
    ''' 対象が空文字からなるテスト '''
    assert all_prefixes('') == set([])

def test_one_char():
    ''' 対象が1文字からなるテスト '''
    assert all_prefixes('a') == set(['a'])

def test_one_word():
    ''' 対象が複数文字、1語からなるテスト '''
    assert all_prefixes('lead') == \
           set(['l', 'le', 'lea', 'lead'])

def test_two_words():
    ''' 対象が2語(途中に空白を含む)からなるテスト '''
    assert all_prefixes('a pen') == \
           set(['a', 'a ', 'a p', 'a pe', 'a pen'])

if __name__ == '__main__':
    nose.runmodule()

10

10個テストケースを思いつけと言われても、難しいですよね。最後に作成したテストケースである、リストの中のリストは捻りだした感じになってしまいました。

# -*- encoding: sjis -*-

import nose

def is_sorted(data):
    if not type(data) == list:
        raise ValueError
    for elem in data:
        # 要素が整数でないものがあった場合
        if not isinstance(elem, int):
            raise ValueError

    copy = data[:]
    return data == sorted(copy)

def test_zero_elem():
    ''' 空リストはリストであり、整数でない値を含まないので
        Trueが返されるべき '''
    assert is_sorted([]) == True

def test_tuple():
    ''' タプルはリストでないのでValueError '''
    try:
        is_sorted((1, 2, 3))
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_list_in_list():
    ''' リストの中にリスト '''
    try:
        is_sorted([1, 4, [5, 7], 8])
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_char_included():
    ''' 要素に文字列を含む場合はValueError '''
    try:
        is_sorted([1, 3 ,'ab2c'])
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_float_included():
    ''' 実数が含まれた場合はValueError '''
    try:
        is_sorted([1, 2, 3.14, 5, 7])
        assert False
    except ValueError:
        pass
    except:
        assert False

def test_one_elem():
    ''' 1要素のみのリスト '''
    assert is_sorted([3]) == True

def test_all_the_same():
    ''' 全部の要素が同じ値の場合 '''
    assert is_sorted([4, 4, 4, 4, 4]) == True

def test_dsc():
    ''' 降順の場合 '''
    assert is_sorted([4, 3, 2 ,1]) == False

def test_asc():
    ''' 昇順の場合 '''
    assert is_sorted([1, 2, 5, 9]) == True

def test_normal():
    ''' 増加・減少がともにある、割と一般的なデータ '''
    assert is_sorted([1, 2, 7, 9, 1]) == False

if __name__ == '__main__':
    nose.runmodule()

2012年6月9日土曜日

TopCoder SRM395 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

あなたは平方数のみからなる数字を使った作業を楽しんでいる。そのような数列は0, 1, 4, 9, 10, 11, 14...となる。この数列のn番目の要素を返すメソッドを作成せよ。先頭の0は0番目の要素とする。

私の解答はこちら。

public class SquareDigitNumbers {

 public int getNumber(int n) {
  int count = 0;
  int number = 0;
  while( true ){
   boolean isSquare = true;
   String snum = "" + number;
   for( int i=0 ; i<snum.length() ; i++ ){
    if( snum.charAt(i) != '0' && snum.charAt(i) != '1' &&
     snum.charAt(i) != '4' && snum.charAt(i) != '9' ){
     isSquare = false;
     break;
    }
   }
   if( isSquare ){
    count++;
   }
   if( count > n ) break;
   number++;
  }
  return number;
 }

}

得点は206.68/250、1回のsubmitでシステムテストクリア。中央値は約190点。0, 1, 4, 9のみからなる数字というのを判別するロジックが重要ですね。

2012年6月8日金曜日

TopCoder SRM394 Div2 250Pts

このTopCoderの問題はこちらで見ることができる(要TopCoder登録 & 問題文は英語)。問題文について説明する。

あなたはareaMap[]という文字列型配列によって形状を示された山にいる。i番目の要素のj番目の文字は0-9の数字であり、それは(i, j)における標高を表している。あなたは以下のルールに従ってこの山を歩く。

  • (0, 0)から動き始める。
  • (i, j)にいるときは(i+1, j), (i, j-1), (i-1, j), (i, j+1)の順に高さを確かめる。そしてまだ入ったことがなく、かつ現在の標高と確かめている位置の標高がheightDifference以下であれば、その方向へ進む。
  • 上記4方向のどこにも動けなくなった時点で歩くのを止める。
上記のルールに従ったとき、歩いた座標の総数を返せ。なお、(0, 0)は最初に1ヶ所歩いたものとみなすものとし、2回以上同じ場所を歩くことはないものとする。

私の解答はこちら。

public class MountainWalk {

 public int cellsVisited(String[] areaMap, int heightDifference) {
  int h = areaMap.length;
  int w = areaMap[0].length();
  boolean[][] isVisited = new boolean[h][w]; // 訪問済ならtrue、未訪問ならfalse
  int hPos = 0; // 現在のx方向の位置
  int wPos = 0; // 現在のy方向の位置
  int nStep = 1; // 歩いた座標の総数
  isVisited[0][0] = true;
  while( true ){
   int[] i = {hPos + 1, hPos, hPos - 1, hPos}; // 探索順序
   int[] j = {wPos, wPos - 1, wPos, wPos + 1};
   int idx = -1; // 訪問できる箇所を表す添字、-1なら訪問できる箇所はないと考える
   for( int k = 0 ; k<i.length; k++ ){
    // フィールドの外にはみ出ず、まだ未訪問の位置で、
    if( isInField(i[k], 0, h) && isInField(j[k], 0, w) && isVisited[i[k]][j[k]] == false ){
     int from = areaMap[hPos].charAt(wPos) - '0';
     int to = areaMap[i[k]].charAt(j[k]) - '0';
     if( Math.abs((from - to)) <= heightDifference ){ // 高さが侵入可能な差しかないなら
      idx = k;
      hPos = i[k]; // その方向へ進む
      wPos = j[k];
      nStep++; // 歩いた座標の数を増やす
      isVisited[i[k]][j[k]] = true; // 訪問済みフラグを立てる
      break;
     }
    }
   }
   if( idx == -1 ) break;
  }
  return nStep;
 }
 private boolean isInField(int i, int from, int to){ // 移動先がフィールド内部か判定
  if( i >= from && i < to ){
   return true;
  }else{
   return false;
  }
 }
}

得点は162.89/250、1回のsubmitでシステムテストクリア。中央値は約148点。250点の問題にしては実装量が多いですね。

2012年6月5日火曜日

Pythonにおける仮引数の取り方

Pythonにおける仮引数のテクニックは主に以下の3種類がある。サンプルコードを通じて、これらの機能について確認していくことにする。

名称 役割
デフォルト引数関数の引数で基本的に与える値が決まっている場合はデフォルト引数を利用するとよい。呼び出しの時に与える引数を減らすことができるので、記述量を減らすことができる。
可変長引数外からいくつ引数を与えても処理してくれる仕組み。
名前付き引数順番を変えても、呼び出す側で明示的に「引数名=値」の形式で指定しておけば引数の順番を関数定義で設定した順序から変えても構わないという仕組み。これにより、大量の引数があるときでも、順序を気にせず指定してもよいことになる。
# -*- encoding: sjis -*-

def all_add(lst, num=1): # num=1がデフォルト引数、デフォルト引数は引数群の後半に書く
    ''' リストの全要素にnumを加える '''
    for i in range(len(lst)):
        lst[i] += num
    return lst

def search_arg_max(dat, *new): # 可変長引数は*newである
    ''' 引数の順序が大事、複数引数を渡す場合、*newの前に引数をすべて定義する必要がある。
        複数の可変長引数は与えられない。可変長引数リストの区切りが分からないからである。
    '''
    if not dat or not new: # どちらかを欠いている場合
        return -1

    # 可変長引数はタプルであることに注意
    return max(max(dat),max(new))

if __name__ == '__main__':
    data = [1, 3, 5, 8, 11]
    print all_add(data) # [2, 4, 6, 9, 12]
    data = [1, 3, 5, 8, 11]
    print all_add(data, 0) # [1, 3, 5, 8, 11] (変化なし)
    data2 = [2, 9, 10]
    # 順序が関数定義とは異なっていても引数に名前をつけて渡せばOK
    print all_add(num=2, lst=data) # [3, 5, 7, 10, 13]
    print search_arg_max(data, 3, 9, 12) # 12

その他、可変長引数で該当する引数が存在しない場合は空のタプルが入力されることにも注意。

2012年6月2日土曜日

2012年5月の学習記録

2012年5月に学習した主な本一覧。

大学生の線形代数(読了)
すごく今更感もあるのですが、直交行列による対角化の意義についてある程度理解できたのは大きいと思いました。
マスタリングTCP/IP(pp.59~98)
3分間ネットワーク基礎講座と並行して読書中。
一般化線形モデル入門(pp.80~109)
急に5章辺りから難易度が増したような。1段難易度を下げた本に先に目を通して、遠回りすることも検討中。
初めてのコンピュータサイエンス(pp.1~244)
Pythonの初めての学習にはよいと思います。一通り全体を読んで、演習まで全部やると50~75時間程度になりそうです。意外と演習量は多いと思います。ライブラリの充実度、コーディング量の少なさもあって、Pythonは便利ですね。Pythonに関連した技術ということで、NumPyやPyR、sphinx辺りにも興味があります。
線形代数キャンパスゼミ(読了)
ジョルダン標準形の簡単な計算法を理解できた。それ以外はもっぱら復習のためだけに読みました。反復しないと記憶に定着しないことを実感中。
統計解析入門(赤平)(pp.88~114)
精読中。
英検2級予想問題ドリル 改訂新版(読了)
2級というのは、高卒程度にしては簡単すぎますね。
Forest 6th edition 解いてトレーニング(読了)
短い期間で英文法を復習するならいい本だと思います。出てくる英単語は簡単なものばかりですので、単語の訓練であれば、別の本を利用する必要があるでしょう。個人的には速読英単語 必修編、ある程度単語を覚えたら次にDUOに手を出していました。話すという能力は別にして、ForestとDUOを95%以上マスターしたらTOEICリーディングは450点近く取れるでしょう。次のTOEIC受験の機会にも解きなおす予定。
入門git(pp.1~45ぐらいとその他掻い摘んで)
githubに成果物を上げるということを試してみたく購入。最初のチュートリアルを試せば、基本的な操作はできるようになれるはず。ProjectEulerはこれを利用して解答を上げていこうかと考えています。一番簡単な操作はできるようになったということで、その他のページは当面読まない予定。
実践ビジネス英語5月号
TOEICのリスニング対策の一環として利用。NHKラジオの中でも一番難しい教材と思うのですが、昔と比較して難易度が落ちたのか予想以上に聞き取れていました。ただ、ネイティブのナチュラルスピードは聞き取れないこともしばしば。

2012年6月1日金曜日

初めてのコンピュータサイエンス第11章解答

第11章の解答。以下ネタバレなので、読みたくない方は立ち去りましょう。問題文は載せませんので悪しからず。必要なら買いましょう。

1

k回探索するものとすると、ソート+探索と、毎回探索するという計算の量について次の式が成り立つ(定数項がないため、すごく大まかですが)。k*log_2(N) + N*log_2(N) <= k*N*log_2(N)(_2は対数の底が2であることを意味する)。これを満たすkはk >= N/(N-1)である。右辺はN=1のとき最大値2をとり、かつ1より大きい。従ってk>=2であればよく、2回探索した時点で毎回未ソートのリストから探索するよりも、ソートしてから探索を繰り返すほうが高速になる。

2

利用したコードは以下の通り。既存のソートアルゴリズムに毎回print文を挟めばよい。

a)の場合

  • [1, 5, 4, 3, 7, 6, 2]
  • [1, 2, 4, 3, 7, 6, 5]
  • [1, 2, 3, 4, 7, 6, 5]
  • [1, 2, 3, 4, 7, 6, 5]
  • [1, 2, 3, 4, 5, 6, 7]
  • [1, 2, 3, 4, 5, 6, 7]
  • [1, 2, 3, 4, 5, 6, 7]
  • [1, 2, 3, 4, 5, 6, 7]

b)の場合

  • [5, 6, 4, 3, 7, 1, 2]
  • [4, 5, 6, 3, 7, 1, 2]
  • [3, 4, 5, 6, 7, 1, 2]
  • [3, 4, 5, 6, 7, 1, 2]
  • [1, 3, 4, 5, 6, 7, 2]
  • [1, 2, 3, 4, 5, 6, 7]
  • [1, 2, 3, 4, 5, 6, 7]
# -*- encoding: sjis -*-

def selection_sort(L):
    ''' L[:i]までソート済みとして、
        L[i]とL[i:]の最小値の交換を繰り返す
    '''
    for i in range(len(L)):
        smallest = L.index(min(L[i:]))
        L[i], L[smallest] = L[smallest], L[i]
        print L
    return L

def insertion_sort(L):
    ''' L[:b]までソート済みとして、
        L[b]をソート済みのリストに混ぜる
    '''
    for i in range(1, len(L)):
        pos = 0
        for j in range(i-1, -1, -1):
            # print L[i], " ", L[j]
            if L[i] >= L[j]:
                pos = j + 1
                # print "pos = ", pos, L[i], L[j]
                break
        L.insert(pos, L[i])
        del L[i+1]
        print L
    return L

3

テストと実装のファイルは分けずに書いていますが、本当なら分けるべきでしょう。

# -*- encoding: sjis -*-

import nose

def swap(L, pos1, pos2):
    tmp = L[pos1]
    L[pos1] = L[pos2]
    L[pos2] = tmp

def bubble_sort(L):
    ''' 1)先頭から隣り合う要素を比較し、添え字の小さいほうが
          大きいようなら値の交換を行う
        2)1)をi回繰り返すと後ろからi個の要素が整列済みになっている
          要素数-1回繰り返す
    '''
    for i in range(len(L)-1, 0, -1):
        for j in range(i):
            if L[j] > L[j+1]:
                swap(L, j, j+1)
        print L
    return L

# run_test とするとテストケースと勘違いされるのでrun_checkとする
def run_check(original, expected):
    bubble_sort(original)
    assert original == expected

def test_empty():
    run_check([], [])

def test_one():
    run_check([3], [3])

def test_reverse():
    run_check([5, 4, 3, 2, 1], [1, 2, 3, 4, 5])

def test_same():
    run_check([2, 2, 2, 2], [2, 2, 2, 2])

def test_random():
    run_check([6, 5, 4, 3, 7, 1, 2], [1, 2, 3, 4 ,5 , 6, 7])

if __name__ == '__main__':
    nose.runmodule()

4

テストコードは上のテストの使いまわしでよいと思います。

def bubble_sort_2(L):
    ''' 1)最後尾から先頭へ向けて隣り合う要素を比較し、添え字の
          小さいほうが大きいようなら値の交換を行う
        2)1)をi回繰り返すと先頭からi個の要素が整列済みになっている。
          要素数-1回繰り返す
    '''
    for i in range(len(L)-1):
        for j in range(len(L)-1, i, -1):
            if L[j] < L[j-1]:
                swap(L, j, j-1)
        print L

5

用意したデータは降順データなので、全部のデータを反転させることになる。結果としては、バブルソートは全体的に遅いということが分かる。また、ソートにかかる時間はO(n^2)に従っていることが確認できる。バブルソートが特に遅いのは、比較の回数が多いためと思われる。一方挿入ソート、選択ソートで実行速度に大きな差は見られなかった。大量データをずらすというコストは、交換するコストとほとんど差がないのではないかと推測される。なお、ch11_sort_algo.pyに上に挙げたbubble_sortなどのソートアルゴリズムを実装している。

import time
from ch11_sort_algo import *

def print_times(L):
    print len(L),
    for func in (bubble_sort, bubble_sort_2, insertion_sort, selection_sort):
        L_copy = L[:]
        t_from = time.time()
        func(L_copy)
        t_to = time.time()
        print "\t%.1f" % ((t_to - t_from) * 1000.),
    print

for list_size in [10, 1000, 2000, 5000, 10000]:
    L = range(list_size)
    L.reverse()
    print_times(L)

6

ソート済みの長さNのリストの場合

手法比較回数コピー回数
挿入ソートN-1回(毎回ソート済みの最終要素に1度アクセスして終了)0回(後ろにつけるだけでコピーは発生しない)
選択ソートN(N-1)/2回(未ソートの部分で最小値探索の必要あり)0回(未ソート部分の先頭が最小値と分かるのでコピー不要)

リストが逆順

手法比較回数コピー回数
挿入ソートN(N-1)/2回(毎回ソート済みのリスト全体の探索の必要あり)0回(コピーではなく値の交換がN-1回発生する)
選択ソートN(N-1)/2回N-1回(コピーするリストの長さは可変)

入力のリストがすべて同じ値の場合

手法比較回数コピー回数
挿入ソートN-1回(毎回ソート済みの最終要素にアクセスして終了)0回
選択ソートN(N-1)/2回0回

7

挿入に伴い発生するリストのコピーにかかるコストを考慮していない。これを考慮するのであれば、オーダーはN^2になる。全体の結論にも影響する。

8

ランダムに並び替えたものが偶然ソート済みになるのは、要素数がNであれば、平均してN!回に1回発生することなのでO(N!)になる。要素が一意であるかが問題になっているのは、一意かそうでないかでオーダーが変化するからである。例えば、N個のうちN-1個は同じ値で1つだけ違う値であれば、O(N)になるし、すべて同じ値であれば、O(1)になるからである。

9

extend呼び出しをなくすにはループ内で結合まで行うしかない。そこで、i1かi2が終端まで着たら他方をすべて結合してループを抜け出すという書き換えを行った。

# -*- encoding: sjis -*-

def merge(L1, L2):
    newL = []
    i1 = i2 = 0

    while i1 != len(L1) or i2 != len(L2): # and から orに変更
        if i1 >= len(L1): # 終端に着いたので他方の残りの要素を結合
            newL += L2[i2:]
            break
        elif i2 >= len(L2):
            newL += L1[i1:]
            break

        if L1[i1] <= L2[i2]:
            newL.append(L1[i1])
            i1 += 1
        else:
            newL.append(L2[i2])
            i2 += 1

    return newL

def mergesort(L):
    workspace = []
    for i in range(len(L)): # 1要素に分解
        workspace.append([L[i]])

    i = 0

    while i < len(workspace) - 1: # マージ開始
        L1 = workspace[i]
        L2 = workspace[i + 1]
        newL = merge(L1, L2)
        workspace.append(newL)
        i += 2

    if len(workspace) != 0:
        L[:] = workspace[-1][:]

    return L

if __name__ == '__main__':
    data = [3, 0, 2, 3, -1]
    print mergesort(data)
    data = [6, 5, 4, 3, 7, 1, 2]
    print mergesort(data)
    data = []
    print mergesort(data)

フォロワー

ブログ アーカイブ

ページビューの合計