2011年12月15日木曜日

TopCoder SRM 525 過去問

TopCoder SRM 525 Div2 Easy と Medium を解きました。

Easy

幅2マス、長さNマスの道がある。マス目は、濡れている場合と乾いている場合がある。
左上からスタートし、右上まで、濡れたマスを通らないように移動したい。
移動は、縦、横、斜めが可能。
可能な場合はYESを、不可能な場合はNOを出力せよ。

解法

まぁ、やるだけです。

提出コード

#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include <cstdio>
#include <functional>
#include <numeric>

using namespace std;

#define REP(i,s,e) for (int i = int(s); i != int(e); i++)
#define rep(i,e) REP(i,0,e)
#define foreach(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)
#define ALL(c) (c).begin(), (c).end()

class RainyRoad {

 public: string isReachable(vector<string> road) {
  rep(i,road[0].length()) if(road[0][i] == 'W' && road[1][i] == 'W') return "NO";
  return "YES";
 }

};


Medium

長方形の上にコインが幾つか乗っている。
プレイヤーは一回につき、上下右左を選び、全てのコインをその方向に動かしてよい。
この操作によって、与えられた長方形からコインがはみ出た場合、コインは落ちて消える。
コインがK枚になるような最小の操作回数を出力せよ。なお、K枚に出来ない場合は-1を出力せよ。

解法

操作を何回行っても、残るのは長方形になる。(或いは何も残らない。。。)
部分長方形を考え、その長方形に対して何枚コインが残るかが分かれば、最小の命令回数は一意に定まる。
例えば、下の行を1つ、上の行を2つ削るような部分長方形を作る場合、下、上、上、上となり、命令回数は四回である。
下と上の順番を逆にしてしまうと、上、上、下、下、下となり、5回となってしまう。
反対側の辺も削るような長方形を作る場合、順番に注意。

提出コード


#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include <cstdio>
#include <functional>
#include <numeric>

using namespace std;

#define REP(i,s,e) for (int i = int(s); i != int(e); i++)
#define rep(i,e) REP(i,0,e)
#define foreach(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)
#define ALL(c) (c).begin(), (c).end()


int cum[51][51];

class DropCoins {

 public: int getMinimum(vector<string> board, int K) {
  int width = board[0].length();
  int height = board.size();
  int ans = -1;
  memset(cum,0,sizeof(cum));
  REP(i,1,height+1) REP(j,1,width+1){
   cum[i][j] = cum[i-1][j] + cum[i][j-1] - cum[i-1][j-1] + (board[i-1][j-1] == 'o');
  }
  rep(y,height) rep(x,width) REP(y2,y,height+1) REP(x2,x,width){
   int coins = cum[y2+1][x2+1] - cum[y2+1][x] - cum[y][x2+1] + cum[y][x];
   if(coins == K){
    int up = y, left = x, down = height - y2 - 1, right = width - x2 - 1;
    int move = 2*min(up,down) + max(up,down) + 2*min(left,right) + max(left,right);
    if(ans == -1 || ans > move){
     ans = move;
    }
   }
  }
  return ans;

 }

};

最近サボりがひどく、いざTopCoderを開いてもついついTwitterを見に行くなどしてしまっていたため、ニコ生で放送しつつ解くということを試しにやってみた。
すると、なんと放送初回から@chokudai 先生が視聴するというミラクルが発生。これにはおいらもびっくり。もっとも、放送している時は全然気付かなかったし、@chokudai 師匠のありがたいアドバイスも勘違いしていて迷走していたのだが(笑)

次はCodeForcesが近いので、久しぶりに出るつもり。

TopCoder SRM 526 過去問

SRM526 の Div2 Easy Medium を解きました。

Easy

ABC三種類の文字からなる文字列が存在する。
先頭からi文字目以降に何種類の文字があるかを格納したvectorを返す。

解法

ケツからみていくだけです。

提出コード

#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <stack>
#include <deque>
#include <queue>
#include <functional>

using namespace std;

#define REP(i,s,e) for (int i = int(s); i != int(e); i++)
#define rep(i,e) REP(i,0,e)
#define foreach(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)
#define ALL(c) ((c).begin(), (c).end())
#define ll long long
#define VI vector<int>

class CheatingQuiz {

	public: vector<int> howMany(string answers) {
		int count = 0;
		bool abc[3];
		memset(abc,false,sizeof(abc));
		vector<int> ans;
		for(int i = answers.length()-1; i >= 0;i--){
			if(count == 3){
				ans.push_back(3);
				continue;
			}
			switch(answers[i]){
			case 'A':
				if(!abc[0]){
					abc[0] = true;
					count++;
				}
				break;

			case 'B':
				if(!abc[1]){
					abc[1] = true;
					count++;
				}
				break;

			case 'C':
				if(!abc[2]){
					abc[2] = true;
					count++;
				}
				break;
			}
			ans.push_back(count);
		}
		reverse( ans.begin(), ans.end() );
		return ans;
	}

};


Medium

n行m列の行列がある。
各行、各列ごとに最大1羽、アヒルが居る。 
アヒルを移動させて、縦でも横でもいいから整列させたい。
アヒルは、移動先が空いていればどこへでも移動が可能で、移動にかかる時間はハミルトン距離によって算出される。
整列するのにかかる最小の時間を求めよ。

解法

どういう整列のさせ方をするかが決まれば、その整列の実現にかかる最小の時間は一意に定まる。
よって、全通り試して最小の時間を返せばよい。
より具体的には、例えば横に並べる場合、最も左に居るアヒルを列の最も左に移動させ、そのアヒルの次に最も左に居るアヒルをその右へ移動させ。。。と整列させる。
縦に並べる場合も同じである。
この整列のさせ方が最小の時間を取ることは、図を書けば直ちに分かる。
実装では、二次元の構造体にアヒルの位置を記憶させ、vectorにしまったのち、xとyそれぞれに対してバブルソートを行わせる関数を実装して対処した。

提出コード


#include <algorithm>
#include <iostream>
#include <map>
#include <numeric>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <stack>
#include <deque>
#include <queue>
#include <functional>

using namespace std;

#define REP(i,s,e) for (int i = int(s); i < int(e); i++)
#define rep(i,e) REP(i,0,e)
#define foreach(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)
#define ALL(c) (c).begin(), (c).end()
#define ll long long
#define VI vector<int>
#define INF 1<<30

typedef struct pos{
	int x;
	int y;

	int hamiltone_distance(int _x, int _y){
		return (abs(x - _x) + abs(y - _y));
	}
};

void sort_column(vector<pos>& s){
	bool recursive = false;
	REP(i,1,s.size()){
		if(s[i-1].y > s[i].y){
			swap(s[i],s[i-1]);
			recursive = true;
		}
	}
	if(recursive) sort_column(s);
}

void sort_row(vector<pos>& s){
	bool recursive = false;
	REP(i,1,s.size()){
		if(s[i-1].x > s[i].x){
			swap(s[i],s[i-1]);
			recursive = true;
		}
	}
	if(recursive) sort_column(s);
}

class DucksAlignment {

	public: int minimumTime(vector<string> grid) {
		int ans = INF;
		int column = grid.size();
		int row = grid[0].length();
		vector<pos> ret;
		rep(i,column) rep(j,row){
			if(grid[i][j] == 'o'){
				pos tmp = {j,i};
				ret.push_back(tmp);
			}
		}
		int sum;
		sort_row(ret);
		rep(i,column) rep(j,row-ret.size()+1){
			sum = 0;
			rep(k,ret.size()) sum += ret[k].hamiltone_distance(j+k,i);
			ans = min(sum,ans);
		}
		swap(column,row);
		sort_column(ret);
		rep(i,column) rep(j,row-ret.size()+1){
			sum = 0;
			rep(k,ret.size()) sum += ret[k].hamiltone_distance(i,j+k);
			ans = min(sum,ans);
		}
		return ans;
	}
};

2011年12月7日水曜日

何故エンジニアを適正に評価出来ない会社に務めてはいけないのか。経済学的に考察する

このエントリはCompetitive Programming Advent Calendar(Day7)用に書かれたものです。

みなさんこんにちは、cacahuatlと申します。
競技プログラミングクラスタのAdvent Calendarですが、私はそんなに実力が高くないし、技術的なことも、他の方々に比べると正直ひよっこ以下です。

なので、私の経済学部出身という地の利(?)を利用して、ちょっと変わった記事を書くことにしました。

たびたび、エンジニアを適正に評価出来ない会社に勤めてはいけないという話を聞きます。
これは何故でしょうか?直感的には合っているような気がしますが、本当でしょうか?
このエントリでは、上記の、「何故エンジニアは自身を適正に評価出来ない会社に勤めてはいけないのか」という問題を経済学的に考察していきたいと思います。

注意:筆者は経済学に関して素人です。経済学者様などにドヤ顔でこの話をするとフルボッコにされる可能性があるため、注意して下さい。

考察するにあたり、現実を単純化した「モデル」を考えます。シムシティやシムズを想像してくれると早いかと思います。

この世界では、エンジニアを雇う企業は2つしかありません。一つは、エンジニアの能力を適切に評価出来る企業。もう一つは、エンジニアの能力を全く評価出来ない企業です。
この世界では、どちらの企業も常に人材が不足していて、エンジニアの求人を常に出しています。どちらの企業も、自分が適正だと思った報酬以上の額をエンジニアに支払うことはありません。

「何だか現実の世界と随分違うなぁ。。。」そう思った貴方。はい、その通りです。しかし、

             /)
           ///)
          /,.=゙''"/
   /     i f ,.r='"-‐'つ____   こまけぇこたぁいいんだよ!!
  /      /   _,.-‐'~/⌒  ⌒\
    /   ,i   ,二ニ⊃( ●). (●)\
   /    ノ    il゙フ::::::⌒(__人__)⌒::::: \
      ,イ「ト、  ,!,!|     |r┬-|     |
     / iトヾヽ_/ィ"\      `ー'´     /


現実を精密に再現する必要は無いのです。現実を単純化し、現実の世界を理解する手がかりを掴めれば、それだけで素晴らしいことであるとは思いませんか?

この不思議な世界では、エンジニアは自分の能力が分かっていますですから当然、自分の能力に対する適正な報酬も分かっています。
企業も同じです。エンジニアを適切に評価出来る企業は、適正報酬が分かっていますし、そうでない企業は、適正報酬も分かりません。

エンジニアを適切に評価出来る企業は、その人材に対してどのくらい払えばいいのか分かっていますから、面接に来た人に対して、その報酬を提示すればいいですよね。
でも、そうでない企業の場合、どうすればいいのでしょう?

。。。と、いうところで。
これはCompetitibe Programming Advent Calendarであることを思い出して。
今回は、競技プログラミング風に、問題を解くことで考えてみましょう。


Div2 Easy 250

Adverse Selection

貴方はエンジニアの能力を適正に評価することの出来ない会社の採用担当をしています。
貴方の会社に雇われる可能性があるエンジニアの能力が、abillityとして与えられます。
エンジニアは、自身のabillityを下回る報酬を提示する会社に雇用されることはなく、常に自分のabillity以上の報酬を支払う会社に雇用されようとします。
貴方の会社はエンジニアを適正に評価することが出来ないため、エンジニアへの報酬として、貴方の会社に雇われる可能性のあるエンジニア達の能力の平均を提示します。
平均は常に整数で、小数点以下は切り捨てられます。
初期状態では、全エンジニアが雇われる可能性のあるものとします。

最終的に貴方の会社にくる可能性があるエンジニア達の能力を返しなさい。


Class :                      Company
Method:                   adverseSelection
Parameters:              vector<int>
Returns:                   vector<int>
Method signature:    vector<int> adverseSelection(vector<int> abillity)

この問題は再帰関数で愚直にシミュレーションを行うことで解けそうです。
実装してみましょう。

#include <vector>

using namespace std;

#define REP(i,s,e) for (int i = int(s); i != int(e); i++)
#define rep(i,e) REP(i,0,e)
#define foreach(i,c) for (typeof((c).begin()) i = (c).begin(); i != (c).end(); i++)

class Company{
public:
 // 平均を計算するヘルパ関数
 int mean(vector<int> ability){
  int sum = 0;
  foreach(iter,ability) sum += *iter;
  return sum/(int)ability.size();
 }

 vector<int> solve(vector<int> ability,int numPeople){
  int reward = mean(ability);
  
  /* 自分の能力より報酬が低ければくる可能性はない。 */
  foreach(iter,ability) if(*iter > reward){ ability.erase(iter); iter--;} 

  /* 来社する可能性があるエンジニアの数が減少していれば、平均が変わるため、再帰する。 */
  return ((int)ability.size() == numPeople)? ability:solve(ability, (int)ability.size());
 }

 vector<int> adverseSelection(vector<int> ability){
  // 再帰一発。
  return solve(ability, (int)ability.size());
 }
};


この解答を実行すると分かりますが、最終的に会社に来る可能性のあるエンジニアは、常に能力の最も低いエンジニアとなり、報酬も最低値を取ります

何故こうなるのか?

初期状態では、全エンジニアは雇われる可能性があります。
これらの平均を報酬として提示すると、平均より高い実力を持ったエンジニア達は「うわっ、私の年収、低すぎ・・・?」となり、雇われようとしなくなります。
結果、雇われる可能性があるのは、平均以下の実力を持ったエンジニアだけになります。
ここで再び、実力の平均を再計算すると、当然、以前の平均よりも低下します。
すると、下がった平均より高い実力を持ったエンジニア達は「うわっ、私の年収、低すぎ・・・?」となって、雇われようとしなくなります。

このループは全てのエンジニアが同じ実力となるまで続き、最終的に実力が最低値の人間のみになると終了します。

つまり、エンジニアの能力を適切に評価出来ない企業は、このモデルにおいて、最低の実力を持ったエンジニアしか雇いません。

このことが指し示すのは、エンジニアの能力を適切に評価出来ない企業で働くと、報酬が低く、周囲にいるエンジニアの能力も低いという劣悪な環境に置かれる可能性が高い、ということです。

なるほど、確かに、エンジニアを適切に評価出来ない企業で働くことは、合理的とは言えなさそうです。

このように、自分が購入しようと考えているものの価値が自分では分からない時、とにかく安いものを求めた結果、品質の悪いものを買ってしまうようなことを、経済学では「逆選択」と言います。

教科書的には、アメリカの中古車市場で品質の悪い車ばかりが出回っている理由を説明する時に、用いられます。

消費者は中古車の品質が分からないので、安い物を買おうとする

品質の良い中古車を売っている業者は、品質に見合った値段を付けても売れないため、市場から退出する

市場には品質が悪く安い車しか残らなかった。

ということが起こったのだと言われているそうです。

競技プログラミングとは殆ど関係の無い記事でしたが、ご愛嬌ということで(笑


最後に一つ申し上げておきますが、本記事は、エンジニアの方をdisるものでは決してありません。念のため。

このような楽しい企画を主催して下さった@_tanzaku_様、有り難うございました。