HackerRank Week of Code 30 参加日記
289位/10554人でレートほぼ変化なし。以下、難易度EasyとMediumのものは省略する。
斜面上にn本のポールがあり、それぞれ高度x[i]と重さw[i]が与えられる(高度はユニーク)。これをk本のスタックにまとめたい。
・スタック位置はもともとポールがあった場所のみ有効
・ポールは下にのみ移動できる
・移動には、(ポールの重さx高度差)のコストがかかる
このときの最小コストを求めよ。
1 <= k < n <= 5000
1 <= w[i], x[i] <= 10^6
ポールは高度順(昇順)で与えられる
以下、ポールの順序を逆転して考える(0が一番上、n-1が一番下。必ず1番下のポール位置を使うことがヒントになる)。
F(n, k)を、ポール0~nでk個のスタックを作ったときの最小コストと定義する。
、とすると、
このように展開すると、まずの部分は予め計算すれば定数時間で求めることができる。
そして、の部分は、独立項、傾きの線集合について、最小値を求めることと同義。傾きは単調増加しているので、Convex Hull Trickを使うといちいちループせずとも効率よく求めることができる。
Convex Hull Trickはこのサイトが分かりやすかった。
http://wcipeg.com/wiki/Convex_hull_trick
class Program { const int MAXN = 5000; static double[] weight = new double[MAXN + 5]; static double[] pos = new double[MAXN + 5]; static double[] A = new double[MAXN + 5]; static double[] B = new double[MAXN + 5]; static double[] F = new double[MAXN + 5]; static double[,] dp = new double[MAXN, 2]; static void Main(string[] args) { int N, K, x, k; var s = Console.ReadLine().Split().Select(v => Int32.Parse(v)).ToList(); N = s[0]; K = s[1]; for (x = N - 1; x >= 0; --x) { s = Console.ReadLine().Split().Select(v => Int32.Parse(v)).ToList(); pos[x] = s[0]; weight[x] = s[1]; } for (x = 0; x < N; ++x) { A[x] = weight[x] + (x > 0 ? A[x - 1] : 0); B[x] = pos[x] * weight[x] + (x > 0 ? B[x - 1] : 0); F[x] = dp[x, 1] = B[x] - pos[x] * A[x]; } var convexHullTrick = new ConvexHullTrick(); for (k = 2; k <= K; ++k) { convexHullTrick.Init(); convexHullTrick.AddLine(A[k - 2], dp[k - 2, (k - 1) & 1] - B[k - 2]); for (x = k - 1; x < N; ++x) { if (k == x + 1) dp[x, k & 1] = 0; else dp[x, k & 1] = F[x] + convexHullTrick.Query(pos[x], true); convexHullTrick.AddLine(A[x], dp[x, (k - 1) & 1] - B[x]); } } Console.WriteLine((double)dp[N - 1, K & 1]); } } public class ConvexHullTrick { class Line { public double A; public double B; public Line(double a, double b) { A = a; B = b; } public double GetY(double x) { return A * x + B; } } Deque<Line> lines = new Deque<Line>(); public void Init() { lines.Init(); } // y = ax + b public void AddLine(double a, double b) { Insert(new Line(a, b)); } double Area(Line a, Line b, Line c) { var ax = (b.A - a.A); var bx = (c.B - a.B); var ay = (c.A - a.A); var by = (b.B - a.B); return ax * bx - ay * by; } bool Cw(Line a, Line b, Line c) { return Area(a, b, c) < 0; } void Insert(Line l) { while (lines.Count > 1 && Cw(lines[lines.Count - 2], lines[lines.Count - 1], l)) { lines.PopBack(); } if (lines.Count == 1) { if (lines[0].B > l.B) lines.PushBack(l); } else { lines.PushBack(l); } } public double Query(double x, bool canRemoveFrontLines = false) { if (lines.Count == 0) { return 0; } if (canRemoveFrontLines) { while (lines.Count > 1 && lines[0].GetY(x) > lines[1].GetY(x)) { lines.PopFront(); } return lines[0].GetY(x); } else { for (int i = 0; i < lines.Count - 1; i++) { if (lines[i].GetY(x) < lines[i + 1].GetY(x)) { return lines[i].GetY(x); } } } return lines[lines.Count - 1].GetY(x); } }
DequeはC++のDequeをC#で実装した個人ライブラリのもの。List等だとTLEになる。
整数の数列が与えられる。次にq個のクエリが"left right x y"の形で与えられる。クエリごとに、数列で以下の条件を満たす要素数を答えよ。
(制約)
1 <= n, q, <= 4 x 10^4
0 <= a[i] <= 4 x 10^4
0 <= left <= right < n
1 <= x <= x x 10^4
0 <= y < x
・xが小さいとき
あらかじめ、x,yのすべての組み合わせについて、a[i]%x==y を満たす要素番号を配列で持てばよい。Left・Rightで二分探索すればO(logN)で求めることができる。
・xが大きいとき
xが大きいとあらかじめ準備することが不可能になる。その代わりa[i]の組み合わせ数が小さくなるので、こちらを列挙できる。同様に、あり得るa[i]の組み合わせ分だけそれぞれ要素番号を持っておけば、Left・Rightでの二分探索が可能になる。
class Program { static int N = 40000; static int K = 200; //sqrt(N) static void Main(string[] args) { Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false }); // [x, y] => list of all indices satisfy a[i] % x == y var pos = new List<int>[K + 2, K + 2]; for (int i = 0; i < K + 2; i++) for (int j = 0; j < K + 2; j++) pos[i, j] = new List<int>(); // [z] => list of all indices satisfy a[i] == z var poss = new List<int>[N + 1]; for (int i = 0; i < N + 1; i++) poss[i] = new List<int>(); int n, q; var str = Console.ReadLine().Split().Select(v => Int32.Parse(v)).ToArray(); n = str[0]; q = str[1]; var a = ReadLine().Split().Select(v => Int32.Parse(v)).ToArray(); for (var i = 1; i < K; i++) { for (int j = 0; j < n; j++) { pos[i, a[j] % i].Add(j); } } for (var i = 0; i < n; i++) { poss[a[i]].Add(i); } for (var i = 0; i < q; i++) { str = Console.ReadLine().Split().Select(v => Int32.Parse(v)).ToArray(); var l = str[0]; var r = str[1]; var x = str[2]; var y = str[3]; int ans = 0; if (x < K) { int st = LowerBound(pos[x, y], l); int en = UpperBound(pos[x, y], r); --en; if (en - st + 1 >= 1) ans += en - st + 1; } else { // iterate through all a[i] candidates that satisfy a[i] % x == y for (int z = y; z <= 40000; z += x) { // count the number within the range int st = LowerBound(poss[z], l); int en = UpperBound(poss[z], r); --en; if (en - st + 1 >= 1) ans += en - st + 1; } } Console.WriteLine(ans); } Console.Out.Flush(); } }
LowerBound()とUpperBound()は個人ライブラリのもの。
以下の2つを定義する。
・無向グラフGの三角形の数を、辺(u, v)・(u, w)・(v, w)がGの辺であるトリプル{u, v, w}(順序は問わない)の数と定義する
・グラフG'を、三角形の数/頂点数が最大となるグラフGのvertex-induced sub-graphと定義する
Gが与えられたとき、G'のいずれか一つを出力せよ。
1 <= 頂点数 <= 50
例として、次のグラフを考える。
これを以下のようなS-Tグラフに変換する。
このグラフをよく観察すると、
・m = 0.1のとき
最大フローは三角形の数より小さい
・m = 0.333...のとき
最大フローは三角形の数より小さい。三角形Aからのフローは1になるが、三角形B+Cからのフローが2に満たないので。
・m = 0.5のとき
最大フローは三角形の数と等しくなる。三角形Aからのフローは1、三角形B+Cからのフローが2になる。
・mがそれより大きいとき
最大フローは三角形の数と等しくなる
となっている。つまり、ちょうどフローと三角形の数が等しくなる場合のmが、求める「三角形の数/頂点数が最大」となることが分かる。よって、mを使って二分探索すれば解が求められる。
最終的な解は、「ちょうどフローと三角形の数が等しくなる場合」よりも少しだけ小さいmを使って最大フローを求めたときの、T側の集合(Maximum Closure Problemの最適解ではないほうの集合)になる。
class Program { static double MAX_FLOW = double.MaxValue; static void Main(string[] args) { var n = Int32.Parse(Console.ReadLine()); // graph var g = new bool[n, n]; for (var i = 0; i < n; i++) { var s = Console.ReadLine().Split().Select(v => v == "1").ToArray(); for (var j = 0; j < n; j++) { g[i, j] = s[j]; } } // triangles var t = new List<Tuple<int, int, int>>(); for (int i = 0; i < n; i++) { for (int j = 0; j < i; ++j) { for (int k = 0; k < j; ++k) { if (g[i, j] && g[j, k] && g[k, i]) { t.Add(new Tuple<int, int, int>(i, j, k)); } } } } int S, T; S = t.Count() + n; T = t.Count() + n + 1; Dinic graph = new Dinic(); double l = 0, r = 50 * 50 * 50; while ((r - l) > 1e-10) { double m = (l + r) / 2; graph.Init(); for (var i = 0; i < n; i++) { graph.AddEdge(S, t.Count() + i, m); } for (var i = 0; i < t.Count(); i++) { graph.AddEdge(t.Count() + t[i].Item1, i, MAX_FLOW); graph.AddEdge(t.Count() + t[i].Item2, i, MAX_FLOW); graph.AddEdge(t.Count() + t[i].Item3, i, MAX_FLOW); graph.AddEdge(i, T, 1); } var x = graph.GetMaxFlow(S, T); if (t.Count() > x) { l = m; } else { r = m; } } var ret = graph.GetStCut(S).Where(v => v >= t.Count() && v != S && v != T).Select(v => v - t.Count()); Console.WriteLine(ret.Count()); foreach (var v in ret) Console.Write((v + 1) + " "); Console.WriteLine(); } }
Dinicは個人ライブラリのもの。
Maximum closure problem
Maximum closure problem の学習まとめ。以下のサイトを参考にした。
Closure problem - Wikipedia
http%3A%2F%2Friot.ieor.berkeley.edu%2F~dorit%2Fpub%2Fscribe%2Flec11%2FLec11posted.pdf&usg=AFQjCNEhubnfNuotTV0_wrbkGGdEF9UHMA&sig2=ewamZFzAoKgnMP5FaFV5cA&bvm=bv.151325232,d.amc *1
Maximum closure problemとは、ノードに重み(正負あり)の付いた有向グラフがあたえられたとき、重みの合計値が最大になるグラフのClosedなサブセットを求める問題である。
Closedのグラフとは、グラフの全ノードについて、その下流ノードがすべてそのグラフに属するもの。たとえば下の図だと、(a)はClosedだが(b)はClosedではない。
(*1より抜粋)
求め方
次のようなS-Tグラフを作成する。
・Sから正の重みがついたノードへ、その重み付きのエッジを追加
・Tから負の重みがついたノードへ、その重み付きのエッジを追加
・それ以外のエッジは、重み∞にする
そしてS-Tの最小カットを行い、S側の集合が求めるサブセットになる。
例えば次のような元グラフがあったとする。
これをS-Tグラフ化したものが以下になる。(点線が最小カット)
S側の集合{b, c, d, e, g}が最大のClosedセットであり、重みの合計値は6になっている。
有向グラフについて、がの最小カットであれば、はの重みの合計が最大になっているclosedセットである。(はから作成したグラフ)
証明
、とする。
(*1より抜粋)
は定数なので、を最小化するということは、の最大化と同じことがわかる。
最小s-tカットの求め方は、以下のスライドが分かりやすかった。
https://www.slideshare.net/KuoE0/acmicpc-minimum-cut
また、Dinicという方法で最小カット(最大フロー)を求める方法はこちらが参考になる。
https://www.slideshare.net/KuoE0/acmicpc-dinics-algorithm
DinicのC#実装
public class Dinic { public class Edge { public int V; public double C; public Edge N; public Edge B; } Edge[] _pool = new Edge[0]; int _topIdx; Edge[] _adj = new Edge[0]; public int MaxIdx; int _minV; public Dinic() { Expand(ref _pool, 128, true); Expand(ref _adj, 128, false); Init(); } public void Init() { for (int i = 0; i < _adj.Length; i++) _adj[i] = null; _topIdx = 0; MaxIdx = 0; _minV = Int32.MaxValue; } void Expand(ref Edge[] edges, int size, bool fillWithEmptyEdge = false) { if (edges.Length <= size) { var newEdges = new Edge[Math.Max(size, edges.Length * 2)]; for (int i = 0; i < edges.Length; i++) newEdges[i] = edges[i]; if (fillWithEmptyEdge) { for (int i = edges.Length; i < newEdges.Length; i++) { newEdges[i] = new Edge(); } } edges = newEdges; } } public void AddEdge(int u, int v, double c, double bc = 0) { MaxIdx = Math.Max(MaxIdx, Math.Max(u + 1, v + 1)); _minV = Math.Min(_minV, Math.Min(u, v)); Expand(ref _pool, _topIdx + 1, true); Expand(ref _adj, MaxIdx); _pool[_topIdx].V = v; _pool[_topIdx].C = c; _pool[_topIdx].N = _adj[u]; _adj[u] = _pool[_topIdx]; _topIdx++; _pool[_topIdx].V = u; _pool[_topIdx].C = bc; _pool[_topIdx].N = _adj[v]; _adj[v] = _pool[_topIdx]; _topIdx++; _adj[u].B = _adj[v]; _adj[v].B = _adj[u]; if (u == v) { _adj[u].N.B = _adj[u]; _adj[v].B = _adj[v].N; } } int[] _d; int[] _q; int _qh; int _qt; Edge[] _cur; public double GetMaxFlow(int s, int t) { MaxIdx = Math.Max(MaxIdx, Math.Max(s + 1, t + 1)); _minV = Math.Min(_minV, Math.Min(s, t)); _d = new int[MaxIdx]; _q = new int[MaxIdx]; _cur = new Edge[MaxIdx]; double f = 0; while (ReLabel(s, t)) { for (int i = 0; i < MaxIdx; i++) _cur[i] = _adj[i]; f += Augument(s, t, s, double.MaxValue); } return f; } bool ReLabel(int s, int t) { for (int i = 0; i < MaxIdx; i++) _d[i] = Int32.MaxValue; _d[_q[_qh = _qt = 0] = t] = 0; while (_qh <= _qt) { var u = _q[_qh++]; for (var i = _adj[u]; i != null; i = i.N) { if (i.B.C != 0 && _d[i.V] > _d[u] + 1) { _d[i.V] = _d[u] + 1; if ((_q[++_qt] = i.V) == s) { return true; } } } } return false; } double Augument(int s, int t, int u, double e) { if (u == t) return e; double f = 0; for (var i = _cur[u]; i != null; i = i.N) { if (i.C > 0 && _d[u] == _d[i.V] + 1) { var df = Augument(s, t, i.V, Math.Min(e, i.C)); if (df > 0) { i.C -= df; i.B.C += df; e -= df; f += df; } } if (!(e > 0)) break; } return f; } // (S, T) public Tuple<List<int>, List<int>> GetStCut(int s) { var used_flg = new bool[MaxIdx]; Dfs(s, used_flg); var ret_s = new List<int>(); var ret_t = new List<int>(); for (int i = _minV; i < MaxIdx; i++) { if (!used_flg[i]) { ret_t.Add(i); } else { ret_s.Add(i); } } return new Tuple<List<int>, List<int>>(ret_s, ret_t); } void Dfs(int u, bool[] used_flg) { if (used_flg[u]) return; used_flg[u] = true; for (var i = _adj[u]; i != null; i = i.N) { if (i.C > 1e-6) Dfs(i.V, used_flg); } } }
CodinGame - Ghost in the Cell 参加日記
CodinGameのコンテストに初参加してみた。ゲームAIを作ってその強さを競うものなのだが、面倒な環境構築が不要でなかなか楽しかった。対戦動画も観ていて面白い。
https://www.codingame.com/replay/194505045
黄色が私。最終的にCyborgの数が多いほうが勝ち
ランキングはリーグ制。最初はウッドリーグ3部からスタートし、リーグボスを倒すごとにウッド2→ウッド1→ブロンズ→シルバー→ゴールドとランクアップする。また、途中でルールが追加されることもある。
ゲームルール概略
- ターン制
- マップ上にいくつかの工場がある
- 工場間は長さの違う道で繋がれている
- 工場は味方・敵・中立のいずれか
- 工場は1ターンごと0~3体のCyborgを自動生産する
- 1ターンごと、味方・敵同時に行動を起こす
- 自工場からほかの工場にCyborg部隊を送ることができる
- 敵または中立の工場にCyborg部隊が到着したら戦いが起こる
- 戦いはCyborg数の多いほう勝利し、数の差分だけ生き残る
- 戦いに勝ったら工場を占領できる
- 最終的に相手を全滅させるか、400ターン後にcyborgの多いほうが勝ち
- 1ターンに複数の部隊を送ることができる*
- Cyborgを10消費して、工場のCyborg生産数を増やすことができる(3まで)*
- 2回まで爆弾を送ることができる*
- 爆弾を被弾すると、工場のCyborg数が半分、または10まで減る(小さいほう)*
(*はリーグが上がるごとに追加されたルール)
実装
1ターンの制限時間が50msしかないので、深く手を読む方針はとらなかった。基本的に、(1)危ない自工場があったら助ける(2)生産力のある中立工場があったら占領しようとする(3)敵に爆弾を送ってみる(4)敵工場を占領しようとする(4)遊んでる工場があったら前線に兵を送る、の優先順位で手の候補をいくつか作成し、そこから21手くらい先までシミュレートして局面評価がベストのものを採用した。
反省
序盤にいろいろ考えすぎて実装が遅れたのが良くなかった。Topcoderのマラソンマッチと違い途中でルールが変わるし、またリーグが落ちることは(たぶん)ない仕組みなので、軽いコードをどんどん書いてSubmitしていったほうがよさそうだ。シミュレーション中の手や評価関数がまったく詰められなかったのも反省点。
結果はぎりぎりシルバーリーグの103位/311人だった。次回はシルバー上位を目標にしよう。
HackerRank Ad Infinitum16 参加日記
始めて参加した。数学関連の問題がでるコンテストらしい。
46位/673人だけど初回参加者用の区分だっただからレベルが低いのかも。
ad Infinitum: 無限に、永久に(ラテン語)
q個の整数が与えられる。それぞれの整数(nとする)について、集合[1,n]内で、一意な素因数の数が最も多いものを算出し、その数を出力せよ。
1 <= q <= 10^5
1 <= n <= 10^18
素数を小さい順に、nを超えるまでかけていけばよい。素数は50くらいまでしか使わないので、最初に列挙しておくと効率が良い。
var q = Int32.Parse(Console.ReadLine()); while (q > 0) { var n = Int64.Parse(Console.ReadLine()); var ret = 0; BigInteger x = 1; foreach (var p in primes) //primesは素数のリスト(昇順) { x *= p; if (x > n) break; ret++; } Console.WriteLine(ret); q--; }
Russian Peasant Exponentiation
q個のクエリが与えられる。それぞれのクエリでは、整数k,m,a,bが与えられる。(a+bi)^kを求めよ(iは虚数単位)。解は、実部と虚部をそれぞれmでModしたものを出力せよ。
大きなべき乗を求める方法は次を参考にせよ
https://www.hackerrank.com/external_redirect?to=http://lafstern.org/matt/col3.pdf
1 <= q <= 10^5
0 <= k <= 10^18
2 <= m <= 10^9
0 <= a, b <= m
「ロシア農民のアルゴリズム」を複素数に応用しろというもの。複素数クラスを作成し、乗算とModを実装すれば、実数と同じコードになる。複素数クラスはNumericsのものだとオーバーフローの危険性がありそうだ。
static void Main(string[] args) { Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false }); var q = Int32.Parse(Console.ReadLine()); while (q > 0) { //a,b,k,m -> (a + bi)^k mod m var str = Console.ReadLine().Split(); var a = Int64.Parse(str[0]); var b = Int64.Parse(str[1]); var k = Int64.Parse(str[2]); var m = Int64.Parse(str[3]); var comp = new MyComplex(a, b); var modPow = ModPow(comp, k, m); //mod前のmの足し忘れ注意! Console.WriteLine(string.Format("{0} {1}", (modPow.Real + m) % m, (modPow.Imaginary + m) % m)); q--; } Console.Out.Flush(); } //ロシア農民のアルゴリズム static MyComplex ModPow(MyComplex x, Int64 n, Int64 mod) { while ((n & 1) == 0) { x = (x * x) % mod; n >>= 1; } var ret = x; n >>= 1; while (n > 0) { x = (x * x) % mod; if ((n & 1) != 0) ret = (ret * x) % mod; n >>= 1; } return ret % mod; } class MyComplex { public Int64 Real; public Int64 Imaginary; public MyComplex(Int64 real, Int64 img) { Real = real; Imaginary = img; } public static MyComplex operator *(MyComplex c1, MyComplex c2) { return new MyComplex(c1.Real * c2.Real - c1.Imaginary * c2.Imaginary, c1.Real * c2.Imaginary + c1.Imaginary * c2.Real); } public static MyComplex operator %(MyComplex c1, Int64 mod) { return new MyComplex((c1.Real) % mod, (c1.Imaginary) % mod); } }
正n角形のパンケーキが与えられる。このパンケーキは、①中心と各頂点を通るもの②中心と各頂点の中間点を通るもの、の2種類の軸が存在する(合計n本)。軸は0~n-1の番号が時計回りに連続でついている。
このとき、n個のクエリが与えられる。クエリの種類は2つ:
タイプ1:軸kで裏返す
タイプ2:パンケーキを、(360*k) / n 度だけ時計回りに回転させる
すべてのクエリが終了したときのパンケーキを、タイプ1またはタイプ2を1回だけ適用させて、初期位置に戻したい。このクエリを答えよ。
3 <= n <= 10^6
1 <= m <= 10^6
0 <= k <= n
ややこしく考えると嵌りそうだが、実際は初期に軸0にあった頂点の位置と、裏返されているか否かだけ管理すればよい。
var str = Console.ReadLine().Split(); var n = Int32.Parse(str[0]); var m = Int32.Parse(str[1]); var x = 2 * n; //位置(頂点+頂点間) var pos = 0; //初期に軸0にあった頂点の位置 var isFlipped = false; while (m > 0) { str = Console.ReadLine().Split(); var k = Int32.Parse(str[1]); switch (str[0]) { case "1": //rotate pos = (pos + k * 2) % x; break; default: //flip pos = (pos + ((k + x - pos) % x) * 2) % x; isFlipped = !isFlipped; break; } m--; } if (!isFlipped) { Console.WriteLine("1 " + ((x - pos) / 2).ToString()); } else { Console.WriteLine("2 " + (pos / 2).ToString()); }
方程式がq個与えられたとき、それぞれ次を満たす最も原点に近い点を求めよ。
・xとyは整数
・xは0より大きい
複数の解があるときは、xのより小さいものを答えよ。
1 <= q <= 10^5
1 <= a <= 10^8
1 <= b <= 10^8
1 <= c <= 10^8
a, b, cは整数
整数解が存在するといことは、cはgcd(a,b)で割り切れる。そして、拡張ユークリッドアルゴリズムを使って一組の(x, y)が計算できたとき、すべての組は次の形で表せる。ベズーの等式 - Wikipedia
ここで、、とすると
と表すことができる。
を最小化するのが目的なので、上式の展開
を最小化すればよい。
とおくと、
になる。よって答えは以下のようになる。(実装は省略)
1. が負のとき
の場合
2. が正のとき
または の場合
m次元の超空間にn人が存在している。この人達は、次元のグリッド線上だけを動くことができる。全員の総移動距離が最小になるよう、空間のある地点に集まりたい。この地点を求めよ。
各人の初期位置は、次元0、次元1...の順にx0,x1...の形で与えられる。
1 <= n <= 10^4
1 <= m <= 10^2
- 10^9 <= xi <= 10^9 (整数のみ)
集合地点の位置は整数
グリッド線上のみ動くことができる(次元間をナナメに移動することはできない)ということは、各次元内で移動が完結しているということになる。よって、次元ごとに集合地点を求めればよい。
ある次元について、移動距離が最小になるのは、真ん中にいる人に集まる場合なので、ソートしてIndexが中点のものが答えになる。
static void Main(string[] args) { var str = Console.ReadLine().Split(); var n = Int64.Parse(str[0]); var m = Int64.Parse(str[1]); var friends = new List<List<Int64>>(); for (int i = 0; i < n; i++) { friends.Add(Console.ReadLine().Split().Select(v => Int64.Parse(v)).ToList()); } for (int i = 0; i < m; i++) { Console.Write(Get(i, friends)); if (i != m - 1) Console.Write(" "); } Console.WriteLine(); } static Int64 Get(int dimension, List<List<Int64>> friends) { var values = new List<Int64>(); foreach (var friend in friends) { values.Add(friend[dimension]); } values.Sort(); return values[(values.Count() - 1) / 2]; }
イントロソートを安定ソートとして習作(C#)
C#で安定ソートを行うときはLinqのOrderBy()が一般的だが、OrderByはクイックソートなのでワースト計算量がO(N^2)になってしまう。ここでは、これを回避したソートを習作してみる。
ちなみにC++のstable_sort()だと、安定マージソートをつかってこの問題を回避している。配列を再帰的に分割していき、長さが閾値(15)未満になったら挿入ソートに切り替えるようだ。
https://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a01347.html#l03409
これをそのまま移植してもOKなのだが、それでは面白くないので、ここではArray.Sort()のイントロソートを強引に安定化することにする。イントロソートは最初はクイックソートを行い、再帰のレベルがある値を超えたらヒープソートに切り替えるといもので、高速なクイックソートの利点を利用しつつ最悪ケースを回避するアルゴリズムだ。基本的にはこの2つのソートを安定版に作り替えればよい。
以下のMS実装を参考にした。
イントロソート(Array.Sort())
https://referencesource.microsoft.com/#mscorlib/system/array.cs,2a2126edd9ca7eb4
クイックソート(Linq)
https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1395017e067e5a34
static public void StableSort<T>(List<T> list) where T : IComparable { int[] map = new int[list.Count()]; for (int i = 0; i < list.Count(); i++) map[i] = i; StableSort(list, map, 0, list.Count() - 1, (int)Math.Floor(Math.Log(list.Count(), 2)) * 2); var tmp = list.ToArray(); for (int i = 0; i < list.Count(); i++) { list[i] = tmp[map[i]]; } } static void StableSort<T>(List<T> list, int[] map, int left, int right, int depthLimit) where T : IComparable { if (left >= right) return; //stable heap sort if (depthLimit == 0) { StableSort_HeapSort(list, map, left, right); return; } //stable quick sort do { int i = left; int j = right; int x = map[i + ((j - i) >> 1)]; do { while (i < list.Count() && CompareKeys(list, x, map[i]) > 0) i++; while (j >= 0 && CompareKeys(list, x, map[j]) < 0) j--; if (i > j) break; if (i < j) { int temp = map[i]; map[i] = map[j]; map[j] = temp; } i++; j--; } while (i <= j); if (j - left <= right - i) { if (left < j) StableSort(list, map, left, j, depthLimit - 1); left = i; } else { if (i < right) StableSort(list, map, i, right, depthLimit - 1); right = j; } } while (left < right); } static void StableSort_HeapSort<T>(List<T> list, int[] map, int lo, int hi) where T : IComparable { int n = hi - lo + 1; for (int i = n / 2; i >= 1; i = i - 1) { StableSort_HeapSort_DownHeap(list, map, i, n, lo); } for (int i = n; i > 1; i = i - 1) { Swap(map, lo, lo + i - 1); StableSort_HeapSort_DownHeap(list, map, 1, i - 1, lo); } } static void StableSort_HeapSort_DownHeap<T>(List<T> list, int[] map, int i, int n, int lo) where T : IComparable { int d = map[lo + i - 1]; int child; while (i <= n / 2) { child = 2 * i; if (child < n && CompareKeys(list, map[lo + child - 1], map[lo + child]) < 0) { child++; } if (!(CompareKeys(list, d, map[lo + child - 1]) < 0)) break; map[lo + i - 1] = map[lo + child - 1]; i = child; } map[lo + i - 1] = d; } static void Swap(int[] map, int i, int j) { var tmp = map[i]; map[i] = map[j]; map[j] = tmp; } static int CompareKeys<T>(List<T> list, int index1, int index2) where T : IComparable { var c = list[index1].CompareTo(list[index2]); if (c == 0) { return index1 - index2; } return c; }
当然、ほとんどの場合でOrderByのほうが少し速い。ワーストケースではこちらのほうが大幅に良いパフォーマンスが出るはず・・・だがうまくテストケースを作れなかった。
平衡二分探索木を使ったsetとmultisetの実装(C#)
C++のset
set
順序付けされたデータを重複を排除して保持するもの。C#のSortedSet
multiset
順序付けされたデータを重複を許容しながら保持するもの。C#に類似のものはない。set
lower_bound()は該当値以上になる最小Indexを、upper_bound()は該当値より大きくなる最小Indexを返す関数で、いずれもO(logN)になる。
どちらも平衡二分探索木で実現できる。平衡二分探索木は二分探索木を、木構造がなるべく偏らないように工夫したものである(偏ると効率が悪くなる=O(N)に近くなる)。ここではRandomized Binary Search Tree(RBST)と呼ばれる、値を追加するときに、なるべく木の高さの偏りがなくなるよう、追加候補の位置に重みづけをしてランダム選択する方法で実装してみる。
詳細はこちらが参考になった
プログラミングコンテストでのデータ構造 2 ~平衡二分探索木編~
/// <summary> /// Self-Balancing Binary Search Tree /// (using Randamized BST) /// </summary> public class SB_BinarySearchTree<T> where T : IComparable { public class Node { public T Value; public Node LChild; public Node RChild; public int Count; //size of the sub tree public Node(T v) { Value = v; Count = 1; } } static Random _rnd = new Random(); public static int Count(Node t) { return t == null ? 0 : t.Count; } static Node Update(Node t) { t.Count = Count(t.LChild) + Count(t.RChild) + 1; return t; } public static Node Merge(Node l, Node r) { if (l == null || r == null) return l == null ? r : l; if ((double)Count(l) / (double)(Count(l) + Count(r)) > _rnd.NextDouble()) { l.RChild = Merge(l.RChild, r); return Update(l); } else { r.LChild = Merge(l, r.LChild); return Update(r); } } /// <summary> /// split as [0, k), [k, n) /// </summary> public static Tuple<Node, Node> Split(Node t, int k) { if (t == null) return new Tuple<Node, Node>(null, null); if (k <= Count(t.LChild)) { var s = Split(t.LChild, k); t.LChild = s.Item2; return new Tuple<Node, Node>(s.Item1, Update(t)); } else { var s = Split(t.RChild, k - Count(t.LChild) - 1); t.RChild = s.Item1; return new Tuple<Node, Node>(Update(t), s.Item2); } } public static Node Remove(Node t, T v) { if (Find(t, v) == null) return t; return RemoveAt(t, LowerBound(t, v)); } public static Node RemoveAt(Node t, int k) { var s = Split(t, k); var s2 = Split(s.Item2, 1); return Merge(s.Item1, s2.Item2); } public static bool Contains(Node t, T v) { return Find(t, v) != null; } public static Node Find(Node t, T v) { while (t != null) { var cmp = t.Value.CompareTo(v); if (cmp > 0) t = t.LChild; else if (cmp < 0) t = t.RChild; else break; } return t; } public static Node FindByIndex(Node t, int idx) { if (t == null) return null; var currentIdx = Count(t) - Count(t.RChild) - 1; while (t != null) { if (currentIdx == idx) return t; if (currentIdx > idx) { t = t.LChild; currentIdx -= (Count(t == null ? null : t.RChild) + 1); } else { t = t.RChild; currentIdx += (Count(t == null ? null : t.LChild) + 1); } } return null; } public static int UpperBound(Node t, T v) { var torg = t; if (t == null) return -1; var ret = Int32.MaxValue; var idx = Count(t) - Count(t.RChild) - 1; while (t != null) { var cmp = t.Value.CompareTo(v); if (cmp > 0) { ret = Math.Min(ret, idx); t = t.LChild; idx -= (Count(t == null ? null : t.RChild) + 1); } else if (cmp <= 0) { t = t.RChild; idx += (Count(t == null ? null : t.LChild) + 1); } } return ret == Int32.MaxValue ? Count(torg) : ret; } public static int LowerBound(Node t, T v) { var torg = t; if (t == null) return -1; var idx = Count(t) - Count(t.RChild) - 1; var ret = Int32.MaxValue; while (t != null) { var cmp = t.Value.CompareTo(v); if (cmp >= 0) { if (cmp == 0) ret = Math.Min(ret, idx); t = t.LChild; if (t == null) ret = Math.Min(ret, idx); idx -= t == null ? 0 : (Count(t.RChild) + 1); } else if (cmp < 0) { t = t.RChild; idx += (Count(t == null ? null : t.LChild) + 1); if (t == null) return idx; } } return ret == Int32.MaxValue ? Count(torg) : ret; } public static Node Insert(Node t, T v) { var ub = LowerBound(t, v); return InsertByIdx(t, ub, v); } static Node InsertByIdx(Node t, int k, T v) { var s = Split(t, k); return Merge(Merge(s.Item1, new Node(v)), s.Item2); } public static IEnumerable<T> Enumerate(Node t) { var ret = new List<T>(); Enumerate(t, ret); return ret; } static void Enumerate(Node t, List<T> ret) { if (t == null) return; Enumerate(t.LChild, ret); ret.Add(t.Value); Enumerate(t.RChild, ret); } }
平衡二分探索木ができてしまえば、set
/// <summary> /// C-like set /// </summary> public class Set<T> where T : IComparable { protected SB_BinarySearchTree<T>.Node _root; public T this[int idx]{ get { return ElementAt(idx); } } public int Count() { return SB_BinarySearchTree<T>.Count(_root); } public virtual void Insert(T v) { if (_root == null) _root = new SB_BinarySearchTree<T>.Node(v); else { if (SB_BinarySearchTree<T>.Find(_root, v) != null) return; _root = SB_BinarySearchTree<T>.Insert(_root, v); } } public void Clear() { _root = null; } public void Remove(T v) { _root = SB_BinarySearchTree<T>.Remove(_root, v); } public bool Contains(T v) { return SB_BinarySearchTree<T>.Contains(_root, v); } public T ElementAt(int k) { var node = SB_BinarySearchTree<T>.FindByIndex(_root, k); if (node == null) throw new IndexOutOfRangeException(); return node.Value; } public int Count(T v) { return SB_BinarySearchTree<T>.UpperBound(_root, v) - SB_BinarySearchTree<T>.LowerBound(_root, v); } public int LowerBound(T v) { return SB_BinarySearchTree<T>.LowerBound(_root, v); } public int UpperBound(T v) { return SB_BinarySearchTree<T>.UpperBound(_root, v); } public Tuple<int, int> EqualRange(T v) { if (!Contains(v)) return new Tuple<int, int>(-1, -1); return new Tuple<int, int>(SB_BinarySearchTree<T>.LowerBound(_root, v), SB_BinarySearchTree<T>.UpperBound(_root, v) - 1); } public List<T> ToList() { return new List<T>(SB_BinarySearchTree<T>.Enumerate(_root)); } }
set
/// <summary> /// C-like multiset /// </summary> public class MultiSet<T> : Set<T> where T : IComparable { public override void Insert(T v) { if (_root == null) _root = new SB_BinarySearchTree<T>.Node(v); else _root = SB_BinarySearchTree<T>.Insert(_root, v); } }
HackerRank Week of Code 28 参加日記
111位/10432人でレート微増だった。Easy問題は省略する。
https://www.hackerrank.com/results/w28/yambe2002
整数xが与えられたとき、以下を満たす整数aの個数を答えよ。
a XOR x > x
0 < a < x
1 <= x <= 10^9
例えばx=b10101111とすると、aの候補は
b0001nnnn
b01nnnnnn
となる(nは1と0どちらでもよい)。よって答えはb0101000になる。
public static long GetAns(long x) { var ret = ~x; var idx = 63; while (idx > 0 && (x >> idx) % 2 == 0) idx--; return ret & (((long)1 << idx) - 1); }
0-9からなる文字列s(長さn)が与えられる。sのサブシーケンスを10進数の数字とみなしたとき、これが8で割り切れるパターン数を求めよ。
1 <= n <= 200000
nが大きいのでサブシーケンスの列挙はできない。8の倍数を次のように場合分けして考える。
・1桁の8の倍数(0,8)
s[i]が該当するパターン数
・2桁の8の倍数(00, 08, ... 96)
s[i]x10+s[j] が該当するパターン数 (j>i)
・3桁以上の8の倍数
3桁の8の倍数(000, 008, ... 992)について
s[i]x100+s[j]x10+s[k] が該当するパターンを列挙(k>j>i)。
下3桁が8の倍数の数字はすべて8の倍数なので、iの位置に応じてパターン数を算出する
static long mod = 1000000007; static void Main(string[] args) { var n = Int32.Parse(Console.ReadLine()); var s = ReadLine(); Console.WriteLine(GetAns(s)); } public static long GetAns(string s) { long ret = 0; var arr = s.Select(c => Int32.Parse(c.ToString())).ToArray(); // take 1 digit foreach (var c in arr) { if (c % 8 == 0) { ret++; ret %= mod; } } // take 2 digits ret += Get2Digits(arr); ret %= mod; // take 3+ digit ret += Get3Digits(arr); ret %= mod; return ret; } public static long Get2Digits(int[] arr) { long ret = 0; var two_digits = Enumerable.Range(0, 13).Select(i => i * 8).ToArray(); //0-96 foreach (var cand in two_digits) { var rightVal = cand % 10; var leftVal = cand / 10; var rightCnt = 0; for (int i = arr.Length - 1; i >= 0; i--) { if (arr[i] == leftVal) { ret += rightCnt; ret %= mod; } if (arr[i] == rightVal) rightCnt++; } } return ret; } public static long Get3Digits(int[] arr) { long ret = 0; var three_digits = Enumerable.Range(0, 125).Select(i => i * 8).ToArray(); //0-992 foreach (var cand in three_digits) { var ldigit = cand / 100; var mdigit = (cand / 10) % 10; var rdigit = cand % 10; long one_cnt = 0; long two_cnt = 0; for (int i = arr.Length - 1; i >= 0; i--) { if (arr[i] == ldigit) { ret += ModPow(2, i, mod) * two_cnt; ret %= mod; } if (arr[i] == mdigit) { two_cnt += one_cnt; two_cnt %= mod; } if (arr[i] == rdigit) one_cnt++; } } return ret; }
1~nで番号付けされたn人の生徒がいる。また、xとyが友達になることを示すペア(x, y)がm個あたえられる。さらに
・xとyが友達のときは、yとxも友達
・xとyが友達でyとzが友達のときは、xとzも友達
とする。
それぞれの生徒の友達の数をすべて合計したものをtotalと呼ぶ。友達になる順番は任意に変えることができる。友達ペアを適用するたびにtotalを求めたとき、その合計の最大を求めよ。
1 <= q <= 16 (クエリ数)
1 <= n <= 10^5
1 <= m <= min(n(n-1)/2, 2 x 10^5)
すべての友達情報を反映させたあと、Totalに影響が少ない順(友達グループの小さい順)に1人ずつ友達から外していって計算する。このとき、閉ループを作る友達情報は先に計算する。グループ情報の保持や閉ループ検出にはUnionFindを使えばよい。
public static long GetAns(int n, List<Tuple<int, int>> friends) { long ret = 0; var unionFind = new UnionFind(); // 閉ループになる友達情報 var relationCreatingClosedLoopNum = 0; foreach (var f in friends) { if (unionFind.IsSameGroup(f.Item1, f.Item2)) { relationCreatingClosedLoopNum++; continue; } unionFind.Unite(f.Item1, f.Item2); } var groups = unionFind.GetGroups().Where(v => v != 0).Select(v => v + 1).ToList(); groups.Sort(); groups.Reverse(); long currentVal = 0; foreach (var g in groups) { currentVal += g * (g - 1); } // 閉ループの分は、情報を削除してもトータルが変わらない ret = currentVal; ret += currentVal * relationCreatingClosedLoopNum; while (groups.Count() > 0) { // 小さいグループから処理 var num = groups.Last(); groups.RemoveAt(groups.Count() - 1); // 該当グループ以外の分 currentVal -= num * (num - 1); ret += currentVal * (num - 1); // 該当グループの分 num--; while (num > 1) { ret += num * (num - 1); num--; } } return ret; } public class UnionFind { private class Node { public Node Parent { get; set; } public int Rank { get; set; } public HashSet<Node> Children; public Node() { Children = new HashSet<Node>(); } } private Dictionary<object, Node> _dict = new Dictionary<object, Node>(); private Node Root(object data) { if (!_dict.ContainsKey(data)) { var node = new Node(); _dict.Add(data, node); return node; } else { var node = _dict[data]; return Find(node); } } private Node Find(Node node) { if (node.Parent == null) return node; return node.Parent = Find(node.Parent); } public void Unite(IComparable x, IComparable y) { var xRoot = Root(x); var yRoot = Root(y); if (xRoot == yRoot) return; if (xRoot.Rank < yRoot.Rank) { ChangeParent(yRoot, xRoot); } else { ChangeParent(xRoot, yRoot); if (xRoot.Rank == yRoot.Rank) xRoot.Rank++; } } void ChangeParent(Node parent, Node childRoot) { if (childRoot.Parent != null) childRoot.Parent.Children.Remove(childRoot); childRoot.Parent = parent; childRoot.Parent.Children.Add(childRoot); foreach (var child in childRoot.Children.ToList()) { ChangeParent(parent, child); } } public bool IsSameGroup(IComparable x, IComparable y) { return Root(x) == Root(y); } public List<long> GetGroups() { var ret = new List<long>(); foreach (var r in _dict.Values.Where(d => d.Parent == null)) { ret.Add(r.Children.Count()); } return ret; } }
横一列に並んだn個のボールがある。ボールは黒か白に色がついている。このとき、k回ボール拾い上げて、なるべく多くの白ボールを拾いたい。拾い上げi回目について(1 <= i <= k)、
・1からn-i+1までの数字xを選ぶ
・端からx番目のボールを拾う。端は左右どちらでもよい
の条件がある。最善手を取ったときの、白ボールの個数の期待値を求めよ。
1 <= k <= n < 30
メモ化再帰で解くだけけだが、本番ではいくつかのケースでTLEになっていた。Dictionaryのキーチェック・値取得を行う部分で
if (dict.ContainsKey(key)) val = dict[key];
のようにしていたのが非効率だった。
dict.TryGetValue(key, out val);
このようにTryGetValue()を使ったほうが、内部的な検索が一回で済むので早いようだ。
参考:
http://stackoverflow.com/questions/9382681/what-is-more-efficient-dictionary-trygetvalue-or-containskeyitem
https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,2e5bc6d8c0f21e67
public static double GetAns(int n, int k, string s) { _n = n; _memo.Clear(); var state = GetState(s); state = (int)Flip(state, n); return GetAns(k, state); } static long Flip(long balls, int n) { var balls2 = Reverse32(balls) >> (32 - n); return Math.Min(balls, balls2); } public static long Reverse32(long value) { var n = value >> 16 | value << 16; n = n >> 0x8 & 0x00ff00ff | n << 0x8 & 0xff00ff00; n = n >> 0x4 & 0x0f0f0f0f | n << 0x4 & 0xf0f0f0f0; n = n >> 0x2 & 0x33333333 | n << 0x2 & 0xcccccccc; n = n >> 0x1 & 0x55555555 | n << 0x1 & 0xaaaaaaaa; return n; } public static int GetState(string s) { var ret = 0; for (int i = 0; i < s.Length; i++) { ret <<= 1; ret |= (s[i] == 'W') ? 1 : 0; } return ret; } public static long Pack(int n, int k, long state) { return state + ((long)n << 32) + ((long)k << 40); } static Dictionary<long, double> _memo = new Dictionary<long, double>(); public static double GetAns(int k, long state) { return Dfs(_n, k, state); } public static double Dfs(int n, int k, long state) { if (state == 0) return 0; if (k == 0) return 0; state = Flip(state, n); double ret = 0; var packed = Pack(n, k, state); // これだとTLEになる・・・ //if (_memo.ContainsKey(packed)) return _memo[packed]; // こっちのほうが早いようだ double result; if (_memo.TryGetValue(packed, out result)) return result; double cnt = 0; double sum = 0; // 1 <= x <= n - i + 1 for (int x = 1; x <= n; x++) { // left var bit = 1L << x - 1; var ball1 = Remove(state, x - 1); var sum_left = Dfs(n - 1, k - 1, ball1) + ((state & bit) != 0 ? 1 : 0); // right bit = 1L << (n - 1 - x + 1); var ball2 = Remove(state, n - 1 - x + 1); var sum_right = Dfs(n - 1, k - 1, ball2) + ((state & bit) != 0 ? 1 : 0); sum += Math.Max(sum_left, sum_right); cnt++; } ret = sum / cnt; _memo.Add(packed, ret); return ret; } static long Remove(long balls, int i) { var bit = 1L << i; balls &= ~bit; var mask = bit - 1; return (balls & mask) | (balls & ~mask & long.MaxValue) >> 1; }
英小文字からなる文字列sがある。各ターンで以下の作業ができるとき、最小何ターンで辞書順最小の文字列に変換することができるか答えよ。
・任意のインデックスiを選ぶ。iは過去に選ばれたインデックスより大きくなければならない
・1回かそれ以上、i以降の文字列をローテートする。方向は左右どちらでもよい
1 <= g <= 100 (sの個数)
1 <= |s| <= 1000
DPで求めることができるが、次の考慮が必要になる。
・ローテーションの作業をインデックスで持つ
たとえばIndex=0の位置で左に3つローテーションする場合、実際に回すと以下のようになるが
abcdef -> defabc (ローテーション)
次のようにインデックス位置の移動で表すことが可能。
abcdef -> abcdef (インデックス位置)
^-----------^---
・小さい文字から削除して処理
sを直接1文字ずつ処理するループ回数が膨大になるが、文字種ごとに処理すれば外のループは26回で済むようになる。さらに、たとえばsがbaacaaadaeでaを削除することを考えると、どの順番で行おうが残る文字列はbcdeになる(ターン回数は3)。そして、次のインデックスはbcdeの位置(0,3,7,9)のいずれかになる。
これらを考慮すると、注目している文字と現在のインデックスだけ保持すればよいことが分かる。
(実装は省略)