- PR -

お手本になるようなソースコード

投稿者投稿内容
sawat
大ベテラン
会議室デビュー日: 2006/08/02
投稿数: 112
投稿日時: 2007-05-10 21:51
引用:

だっちょさんの書き込み (2007-05-10 19:54) より:
 私が作ったのは
http://hp.vector.co.jp/authors/VA007799/viviProg/doc5.htm
のアルゴリズムから1文字のみの一致は評価点を下げ、特異点での値だけを覚えるようにしてテーブル(リスト)を小さくしたもので、通常のデータだと2%くらいのサイズになりました。


まったく同じページ(とそのオリジナル論文)を参考にして、Javaでdiffのようなものを作ったことがあります。それほど大きなファイルを扱わなくてもよかったので(最大で1Mbyteぐらい)大丈夫でしたが、GNUのdiffと比べると遅かったです。
GNUなどのdiffは厳密解を保障しない代わりに、色々な最適化をしているみたい。
Anonymous Coward
会議室デビュー日: 2007/05/09
投稿数: 8
投稿日時: 2007-05-10 23:53
スレ主です。たくさんのレスありがとうございます。
正直、一日でここまでのびるとは思っていませんでした。

しかし、スレを立てた動機を具体的に書かなかったために、
一部の方に要らぬ誤解を与えてしまったようで、申し訳ございません。
動機をちゃんと書きます。

まず、私はとあるソフトウェア会社のプログラマで、
現在は主にVB.NETやC#を使って業務アプリケーションを開発しており、
少なくとも現時点では新人教育には携わっておりません。

しかし、自部署に配属された新人さんや協力会社さんの面倒を見ることはあり、
彼らにVB.NETやC#を覚えてもらうために簡単な課題を考えたりすることはあります。

#私の会社では入社後1〜2ヶ月の新人研修があり、社員が講師を務めることもありますが、
#今のところ私はそういうことはやっていない、という意味です。
#しかし、よく考えると教育に活用するつもりがない、と書いたのは嘘になってしまいますね。
#本当にごめんなさい。
#これは、自分が教育を専門の仕事とする人間ではないという意味で書いたのでした。

そして、それを採点するとき、自分なりに理想解を考えるのですが、
拡張性、可読性、実行速度、生産性などのバランスを考慮しつつ、
なおかつ見やすい表示形式やわかりやすいエラーメッセージなど
ユーザビリティの面も考慮しつつ、とやっていると、意外と奥が深いことに気づきます。

自分が新人の頃には気づかなかったのですが、
普段手がけている業務アプリより遥かに単純で簡単な課題の中に、
とても重要なポイントがあったことを再発見できたりして、
いい意味で息抜き、頭の体操となっています。

しかし、毎年新人が入るような大きな部署ではありませんので、
年中そういうことをやっているわけではなく、一人遊び状態になることもあるわけです。

それで、こういうのは複数の人間でやったらもっと面白いだろうな、と思ったのが、
このスレを立てるに至った動機です。

そういうことは自社内でやれ、というご意見もあるかもしれませんが、
同じ部署内で私より下の立場の人間に対しては、私の方が教える立場ですから、
私の方が驚かされるような斬新なアイデアが出てくる確率は0ではないにしろ少なく、
逆に上の立場の人間に対しては、仕事してないことがばれてしまいます(笑)。

従って、直接の動機は本当に「息抜き/頭の体操」だったのです。

引用:

unibonさんの書き込み (2007-05-10 10:20) より:
(1) 「その枚数が最小」というのが自明なような気もするけど、設計段階かコーディングの時点のどこかで、アルゴリズムを考えてそれが正しいことを証明しないといけないわけですよね。こういうアルゴリズムレベルの仕様はどう書けばいいもんでしょう。
(2) この仕様どおりに作った後、運用直前になって、「あっ、2千円札の扱いはどうするんだ?」などで、そういった仕様の抜けが発覚して悩むかも。(2千円札の存在そのものを無視するのなら楽ですが。)


(1) アルゴリズムレベルの仕様はフローチャート等を使うのが一般的ですが、この会議室のシステムでフローチャートを書くのは大変ですから、他の方が理解しやすく書きやすいもの、といえば、ソースコードが一番なのかなと思います。ノーデバッグバージョンでも構いません。
(2) こういう場合も想定して、あらかじめ拡張性(あるいは柔軟性、保守性)を考えとく必要がでてくるわけです。

引用:

unibonさんの書き込み (2007-05-10 11:33) より:
たとえばいじわるですが、将来もしも2千円札が廃止されて代わりに8千円札が登場した場合、6万4千円を支払うとき、困りませんか?

これを、お題 Ver.2 とします。w


こういうのが出てくるから面白いです。この発想は私にはありませんでした。
#Ver.2までは私もついていけましたが、「金種を任意に指定可能(Ver.3?)」は、
#もはやプログラム以前に数学の世界ですね。私にはちょっとハードルが高いです。

引用:

未記入さんの書き込み (2007-05-10 12:36) より:
>こんばんは。日頃この会議室は参考にさせて頂いております。

ってわりには初投稿だし、


実は私の会社、掲示板系のサイトにPOSTフィルタがかかってまして、
会社からは読むことはできても投稿することはできないんです。

仕事は家に持ち帰らないことにしており、
#個人の主義でもありますが、情報漏洩対策で会社の方針でもあります。
仕事関係で直接投稿したことはなかったのですが、
必要な情報を探しているとInsider.NET会議室のスレッドに行き着くことが多々あり、
かなりの頻度でお世話になっております。

実はスレを立てた動機として、その恩返し的な意図もちょっとだけありまして、
純粋に楽しんで頂けましたら幸いです。
#Cafeは最近閑散としてましたので。
また、上記事情により投稿が夜だけになることをご了承ください。

引用:

lalupin4さんの書き込み (2007-05-10 18:49) より:
 どうしましょ。次のお題募ります?


私もVer.2にC#にて鋭意チャレンジ中ですので、もう少し待って頂けないでしょうか(汗)
かつのり
ぬし
会議室デビュー日: 2004/03/18
投稿数: 2015
お住まい・勤務地: 札幌
投稿日時: 2007-05-11 00:28
新人教育なんかで今回のようはお題を出すと、8000円の場合は?
などというような問題が出たときに、かなり高度な解決方法が要求されますね。
新人教育では教える側、課題を出す側のスキルも重要なんだなと感じました。

コードを日常書いている人向けの息抜きを考えるなら、
もっと面白いお題とかもありますよ。例えば簡単なお題でも
・whileループは1回まで
・forループ禁止
・if/throw/3項演算子禁止
等など・・・色々制約付きでやると、面白いと思いますよ。
カーニー
ぬし
会議室デビュー日: 2003/09/04
投稿数: 358
お住まい・勤務地: 東京
投稿日時: 2007-05-11 10:30
お題

いつも使っているクレジットカードを解約するので、貯まっているポイントを全部使い切ろうと思います。
ところが魅力的な景品がありません。
景品をネットオークションで換金すればよいのですが、出品の手間が面倒なので、なるべく少ない個数の景品で、ポイントを余すことなく使い切ることにしました。

なお景品は11種類あり、それぞれの引き換えに必要なポイントは以下の通りです。

10000pt, 8000pt, 5000pt, 2000pt, 1000pt, 500pt, 100pt, 50pt, 10pt, 5pt, 1pt

Xポイント貯まっているとき、どのような組み合わせで景品に交換したらよいでしょうか?
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2007-05-11 12:46
引用:

わちゃさんの書き込み (2007-05-10 17:33) より:
自分のやつは、実際の日本円と同じ金種でやって、64001 円だと、
そこそこのスピードで、できますが、63999円だと、とたんに
計算回数が増えて、52万回も再帰呼び出しがありました。


「お題 Ver.2」対応版を、独自に Java で作ってみました。(クレジットカードのポイント計算にもそのまま使えるはずです。)
(要求仕様は、あくまでも枚数が最小になることを最優先します。最小枚数でとりうる解が複数あればすべて出力します。)

時間はかかりますが、上記例の 63999円も計算できました。

アーキテクチャの概略はつぎのとおりです。
・金種がnとおりなら、n次元の空間をしらみつぶしに探す。ネスト数が n のループでひたすら回すだけ。再帰は使わない。
・ただし、たとえば40000円を支払う場合、8000円札×5枚で支払うよりも10000円札×4枚で支払うほうが絶対に(枚数的に)得だから、8000円札×5枚のバリエーションは捨てる。(しかし、8000円札×6枚のバリエーションは捨てない。)ほかの札や硬貨も同様。これでループ数を削減する。

コーディングレベルでは、ちょっとバグっぽいかも。もうちょっとキレイに書ければよいのですが。

コード:
import java.util.*;

public class Test {
	
	/**
	 * 札・硬貨の種類
	 * 降順であること
	 */
	private static int[] prices = {10000, 8000, 5000, 1000, 500, 100, 50, 10, 5, 1};
	
	/**
	 * 支払う金額
	 */
	private static int target = 63999;

	/**
	 * ある時点で分かっている最小枚数
	 */
	private static int minMai = Integer.MAX_VALUE;

	/**
	 * 最小枚数の結果(複数あることもあるのでリストになっている)
	 */
	private static List<int[]> results = new ArrayList<int[]>();

	/**
	 * 枚数を算出する。
	 * 
	 * @param amounts
	 * @return
	 */
	private static int calcMai(int[] amounts) {
		int mai = 0;
		for (int i = 0; i < amounts.length; i++) {
			mai += amounts[i];
		}
		return mai;
	}

	/**
	 * level 以下(等しいものは含む)を計算する。
	 * 
	 * @param level
	 * @param amounts
	 * @return
	 */
	private static int calcCand(int level, int[] amounts) {
		int cand = 0;
		for (int i = 0; i <= level; i++) {
			cand += prices[i] * amounts[i];
		}
		return cand;
	}

	/**
	 * level 以上(等しいものは含む)をクリアする。
	 * 
	 * @param level
	 * @param amounts
	 */
	private static void clearAmounts(int level, int[] amounts) {
		for (int i = level; i < amounts.length; i++) {
			amounts[i] = 0;
		}
	}

	/**
	 * この level で、この amount の場合、 これより小さい(上位層)の level に両替(まとめることが)できるか。
	 * 
	 * @param level
	 * @param amount
	 * @return
	 */
	private static boolean isExchangeable(int level, int amount) {
		if (amount == 0) {
			return false;
		}

		int total = prices[level] * amount;
		for (int i = 0; i < level; i++) {
			if (total % prices[i] == 0) {
				return true;
			}
		}
		return false;
	}

	/**
	 * level 以下(等しいものは含む)を出力する。
	 * 
	 * @param level
	 * @param amounts
	 */
	private static void output(int level, int[] amounts) {
		String s = "";
		for (int i = 0; i <= level; i++) {
			if (i > 0) {
				s += " + ";
			}
			s += prices[i] + "*" + amounts[i];
		}
		System.out.println(s);
	}

	/**
	 * つぎのループ変数を算出する。
	 * 
	 * @param level
	 * @param amounts
	 * @return
	 */
	private static int nextLevel(int level, int[] amounts) {
		int cand = calcCand(level, amounts);
		int diff = target - cand;
		/* 除算は切り捨て*/
		int inc = diff / prices[level];
		if (inc == 0) {
			if (level + 1 < prices.length) {
				level++;
			} else {
				amounts[level] += 1;
			}
		} else {
			if (isExchangeable(level, amounts[level])) {
				amounts[level] += inc;
			} else {
				if (level + 1 < prices.length) {
					level++;
				} else {
					amounts[level] += inc;
				}
			}
		}
		return level;
	}

	/**
	 * 中間結果を保存する。
	 * なお、既知の最小枚数より大きければ保存する必要はない。
	 * 
	 * @param amounts
	 */
	private static void preserve(int[] amounts) {
		int mai = calcMai(amounts);
		if (minMai >= mai) {
			if (minMai > mai) {
				minMai = mai;
				results.clear();
			}
			results.add((int[]) (amounts.clone()));
		}
	}
	
	/**
	 * メイン
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		int[] amounts = new int[prices.length];
		clearAmounts(0, amounts);

		/* ループのネストレベル(0 = もっとも外側(10000円), prices.length - 1 = もっとも内側(1円) */
		int level = 0;
		/* 試行したループ回数(参考のためだけ) */
		long count = 0;
		while (true) {
			count++;
			int cand = calcCand(level, amounts);
			if (cand >= target) {
				if (cand == target) {
					preserve(amounts);
				}
				level--;
				if (level < 0) {
					break;
				}
				clearAmounts(level + 1, amounts);
				amounts[level] += 1;
			} else {
				level = nextLevel(level, amounts);
			}
		}
	
		System.out.println("loop count = " + count);
		System.out.println("target = " + target);
		for (int[] result : results) {
			output(result.length - 1, result);
		}
	}
}



--
unibon {B73D0144-CD2A-11DA-8E06-0050DA15BC86}
びしばし
大ベテラン
会議室デビュー日: 2002/03/13
投稿数: 181
投稿日時: 2007-05-11 13:33
ああ、空気読めずに「とっくに議論されてますよ」なんて。失礼しました。

んじゃ私も。
ほかの方のプログラムをあまり読んでいないので既出かもしれませんが、
私なら「広さ優先探索」を採用したいですね。

--
合計枚数1枚,2枚,3枚,...で表すことができる金額(と枚数の情報)の集合をすべて保持してしまい、ゴールの金額が現れたら終了。

例: 説明のために金種を 1,2,5,10 とします。

「n枚までで表現できる金額」は「(n-1)枚までで表現できる金額」のそれぞれに1,2,5,10を1枚足すことで得られる。このとき、既出ものは省く。

1枚だけ: 1(1), 2(1), 5(1), 10(1) ()内の数字が枚数
2枚まで: 1(1), 2(1), 3(2), 4(2), 5(1), 6(2), 7(2), 10(1), 11(2), 12(2), 15(2), 20(2)
3枚まで: 1(1), 2(1), 3(2), 4(2), 5(1), 6(2), 7(2), 8(3), 9(3), 10(1), 11(2), 12(2), 13(3), 14(3), 15(2), 16(3), 17(3), 20(2), 21(3), 22(3), 25(3), 30(3)
以下略

1番高額の物を除いた、各金種の最大使用枚数は、上位の金種との最小公倍数によって頭打ちになりますから、計算量はそうそう爆発しない、と思います...たぶん。

(17:37 コードを追加)
コード:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
* 金種計算 Ver.2
*/
public class Sample2 {
// 金種定義
static final int[] unit = {
1, 5, 10, 50, 100, 500, 1000, 5000, 8000, 10000,
};

/**
* 金額を表現する実際の金種枚数を保持するクラス
*/
static class Amount {
int[] counts = new int[unit.length]; // 各金種の使用枚数
int sum = 0; // 実際の金額

public Amount() {
// 金額0
}

// コピーコンストラクタ (一応)
public Amount(Amount other) {
copyFromOther(other);
}

// 元の金額に1枚追加した状態を作成
public Amount(Amount base, int addIndex) {
copyFromOther(base);
this.counts[addIndex]++;
this.sum += unit[addIndex];
}

// ほかのAmountオブジェクトから内容をコピー
private void copyFromOther(Amount other) {
System.arraycopy(other.counts, 0, this.counts, 0, unit.length);
this.sum = other.sum;
}

// 金額はいくら ?
public int getValue() {
return sum;
}

// HashMap管理用ハッシュコード...金額がユニークなんだし、これでいいよね
public int hashCode() {
return sum;
}

// 同値判定用
public boolean equals(Object o) {
Amount other = (Amount) o;
return this.sum == other.sum;
}

// 結果出力用
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < unit.length; i++) {
sb.append(unit[i] + " : " + counts[i] + "\n");
}
return sb.toString();
}
}

/**
* main()
*/
public static void main(String[] args) {
final int goal = Integer.parseInt(args[0]); // 表現したい金額

HashMap<Integer, Amount> hashMap = new HashMap<Integer, Amount>(); // HashMapで管理
// key: 金額, value: 表現状態

// 初期状態: 金額0 のものを入れておく
hashMap.put(0, new Amount());

while (true) { // 見つかるまでループ
List<Amount> list = new ArrayList<Amount>(); // 「n枚」で実現できるものを一時保管 → 後で一括追加

for (int key : hashMap.keySet()) { // 「n-1枚まで」で実現できる組み合わせでループ
Amount baseValue = hashMap.get(key);

for (int i = 0; i < unit.length; i++) {
Amount value = new Amount(baseValue, i); // n枚目を足した状態

if (hashMap.containsKey(value.getValue())) { // 既出ならスキップ
continue;
}

if (value.getValue() == goal) { // 解答発見 -> 表示して終了
System.out.println(value.toString());
System.exit(0);
}

list.add(value);
}
}

// 「n-1枚まで」で実現できるもの + 「n枚」で実現できるもの == 「n枚まで」で実現できるもの
for (Amount value : list) {
hashMap.put(value.getValue(), value);
}
}
}
}


あらら、なんでこんな改行状態に... ?


[ メッセージ編集済み 編集者: びしばし 編集日時 2007-05-11 17:47 ]
ひろれい
ぬし
会議室デビュー日: 2006/03/02
投稿数: 486
お住まい・勤務地: 万博開催地
投稿日時: 2007-05-11 14:46
Ver.2 を考えてみました(今頃w
言語は、VB.NET。

僕は自他共に認めるヘボ技術者なので、ベタな処理です(^_^;)
なんかアップしにくい雰囲気を打破しにきましたw
叩けるところは叩いていただくと勉強になるので嬉しいです。

コード:
        Dim 金種リスト As Integer() = {10000, 8000, 5000, 2000, 1000, 500, 100, 50, 10, 5, 1}
        Dim 最適支払方法 As Integer() = New Integer(金種リスト.Length - 1) {}
        Dim 開始金種 As Integer = 0
        Dim 試行金額 As Integer = 0
        Dim 金額 As Integer = Integer.Parse(txtIn.Text)
        Dim 最適枚数 As Integer = Integer.MaxValue
        '        Dim 結果 As New StringBuilder

        ' 開始する金種を決定
        For i As Integer = 0 To 金種リスト.Length - 1
            If 金額 >= 金種リスト(i) Then
                開始金種 = i
                Exit For
            End If
        Next

        ' 開始金種の大きい枚数から順にチェック
        For i As Integer = 金額 ¥ 金種リスト(開始金種) To 0 Step -1
            Dim 支払方法 As Integer() = New Integer(金種リスト.Length - 1) {}
            Dim 枚数 As Integer = 0

            試行金額 = 金額 - 金種リスト(開始金種) * i
            枚数 += i
            支払方法(開始金種) = i
            '            結果.Append(金種リスト(開始金種) & "円×" & i & "枚、")

            ' 割り切れたら、即終了
            If 試行金額 = 0 Then
                最適枚数 = 枚数
                最適支払方法 = 支払方法
                Exit For
            End If

            ' 金種を順にチェックしていく
            For j As Integer = 開始金種 + 1 To 金種リスト.Length - 1
                Dim k As Integer = 試行金額 ¥ 金種リスト(j)

                試行金額 = 試行金額 - 金種リスト(j) * k
                枚数 += k
                支払方法(j) = k
                '                結果.Append(金種リスト(j) & "円×" & k & "枚、")

                ' 割り切れたら、終了
                If 試行金額 = 0 Then
                    Exit For
                End If
            Next

            If 枚数 < 最適枚数 Then
                最適枚数 = 枚数
                最適支払方法 = 支払方法
            End If
        Next



#¥マークがダブるので全角を使用。
わちゃ
大ベテラン
会議室デビュー日: 2005/12/05
投稿数: 162
お住まい・勤務地: 東京
投稿日時: 2007-05-11 20:02
びしばしさんの、広さ優先探索だと、悩ましい事に
1000万円とかの支払いで、すごい時間がかかりそう。
# 実行してないので、分かりませんが。

ひろれいさんのだと、{100,50,49,48,47,1} で、141 円払う場合なんか、
ダメみたい。
最初の金種は、全通り試しているみたいなんですが、その次の金種を
減らした方がいい場合には、もれが出ちゃうみたいです。

でも、再帰を使わない方がメモリにはやさしくなりそうですよね。


そういえば、私は、昔、情報系の嫌いな、数学専攻の学生でした。

情報系の必修科目があって、担当教授に出席しなくても、
レポートだけで、単位をくれないか交渉したところ、
アッカーマン関数を再帰を使わずにかけたら、
単位をやると言われました。

結局、できなくて、しぶしぶ講義に出席したのを覚えています。

たぶん、もりあがる話題ではないとは、思いますが、
気が向いたら、やってみると面白いかもしれません。
(言語については、FORTRAN を指定されました)

スキルアップ/キャリアアップ(JOB@IT)