C - Snuke Festival
はじめに
競技プログラミング、AtCoder Beginner Contest 077
C - Snuke Festival
言語はC#
何かあればTwitter→@pirorirori_n712まで
問題
問題文
今年もすぬけ祭の季節がやってきました。 りんごさんは、まず手始めにすぬけ君召喚の儀式を執り行おうと思っています。 儀式には祭壇が必要で、祭壇は上部、中部、下部の 3 つのカテゴリーのパーツ 1 つずつからなります。 祭壇の 3 カテゴリーのパーツがそれぞれ N 個ずつあります。 i 個目の上部のパーツのサイズは Ai 、i 個目の中部のパーツのサイズは Bi 、i 個目の下部のパーツのサイズは Ci です。 祭壇を作るにあたっては、中部のパーツのサイズは上部のパーツのサイズより真に大きく、下部のパーツのサイズは中部のパーツのサイズより 真に大きくなければなりません。 逆に、この条件を満たす任意の 3 つのピースを組み合わせて祭壇を作ることができます。 りんごさんが作ることのできる祭壇は何種類あるでしょうか。 ただし、2 つの祭壇が異なるとは、上部、中部、下部に使われるピースのうち 少なくとも 1 つが異なることを言います。
制約
・1≤N≤10^5 ・1≤Ai≤10^9(1≤i≤N) ・1≤Bi≤10^9(1≤i≤N) ・1≤Ci≤109(1≤i≤N) ・入力は全て整数である
入力
入力は以下の形式で標準入力から与えられる。 N A1 ... AN B1 ... BN C1 ... CN
出力
りんごさんが作ることのできる祭壇の種類数を出力せよ。
解き方
以下を参考にしています。
https://img.atcoder.jp/arc084/editorial.pdf
https://atcoder.jp/contests/abc077/submissions/4561061?lang=ja
解き方の流れ
まず、条件に
「中部のパーツのサイズは上部のパーツのサイズより真に大きく、下部のパーツのサイズは中部のパーツのサイズより 真に大きくなければなりません」
とあることから
祭壇のできるパーツの組み合わせは全て
Ai<Bi<Ciであることが分かります。
ここから、
Biを決めてしまえば
AiはBiよりも小さい値、
CiはBiよりも大きな値の個数を数えられれば
各Biに対する祭壇を作れる個数を出すことができ
全て足し合わせれば
問題の答えとなる「りんごさんが作ることのできる祭壇の種類数」を出すことができます。
解き方の流れ
①Biを1つ決める
②Biよりも小さいAi、Biよりも大きいCiの個数をそれぞれ出す
③ ②で出したAiとCiの個数をかけて、①で選んだBiを使用した際に作れる祭壇の種類数を出す
④残りの各Biに対して①~③を繰り返す
⑤ ③で出した各Biを使用した際に作れる祭壇の種類数を全て足し、問題の答えを求める
実装する
この問題を実装する際、
どのように
Biよりも小さいAiの個数、
Biよりも大きいCiの個数を
出すかが問題になります。
これは解き方の流れの
②Biよりも小さいAi、Biよりも大きいCiの個数をそれぞれ出す
にあたります。
Biよりも小さい数字の個数、
Biよりも大きい数字の個数を求めたいので
Ai、Ciの配列を小さい順に並び替えれば
二分探索を使うことができます。
二分探索 - Wikipedia
~よりも小さい数字の個数
まずは
Biよりも小さい数字の個数が欲しい
Aiの個数を出す二分探索を考えます。
コードは以下です。
//引数 value=Bi array=Aiの配列 static long LowerBound(long value, long[] array) { //下は0でスタート var low = 0; //上は配列の長さでスタート var high = array.Length; //highがlow以下になるまで探索を繰り返す while (low < high) { //中間地点(index) var middle = (high + low) / 2; //valueが中間地点以下であれば上を中間地点に引き下げる if (value <= array[middle]) high = middle; //中間地点よりもvalueが大きければ下を中間地点に引き上げる else low = middle + 1; } return low; }
まず引数は
long value = Bi
long[] array = Aiの配列
です。
呼び出し元では
返ってきた値をそのまま
Biよりも小さいAiの個数として扱っています。
中身は基本的には二分探索と同じですが
異なる部分がいくつかあります。
まず、
引数で渡されるBiと同じ数字が、
Aiの配列の中にも可能性があるので
条件式にイコールをつける必要があります。
//valueが中間地点以下であれば上を中間地点に引き下げる if (value <= array[middle]) high = middle;
このイコールがないと
問題によってはいつまでも条件分岐の中に入ることができずに
無限ループしてしまいます。
では、等符号が入る位置を変えて
//中間地点よりもvalueが小さければ上を中間地点に引き下げる if (value < array[middle]) high = middle; //valueが中間地点以上であれば下を中間地点に引き上げる else low = middle + 1;
とするのはどうかというと
WAしてしまいました。
//中間地点よりもvalueが小さければ上を中間地点に引き下げる if (value < array[middle]) high = middle; //valueが中間地点以上であれば下を中間地点に引き上げる else low = middle;
としてもTLEしてしまうので
一番上で示した形でないとうまく答えが出ないようです。
このメソッドの想定としては
low=value或いはlowがvalueよりも一つ大きい値になっていると思います。
理由はlowが配列のindexだからです。
このメソッドの呼び出し元では
返ってきた値をそのまま
Biよりも小さいAiの個数として扱っているので
返ってくるlowのindexよりも
Biよりも小さい個数は一つ少ないことになります。
例えば、
low=3で返ってきた場合、
lowはindexなので個数としては0,1,2,3の4つないとおかしいですが
実際は3つで計算されるので
Aiのindexが3の値はBiよりも小さくない値であることになります。
これらを色々そろえるために
コード中の等符号の位置が決められていたり
「low = middle」だけでいいようにみえる場所に
+1の処理が入っていると思われます。
~よりも大きい数字の個数
次に
Biよりも大きい数字の個数が欲しい
Ciの個数を出す二分探索を考えます。
コードは以下です。
//引数 value=Bi array=Ciの配列 static long UpperBound(long value, long[] array) { //下は0でスタート var low = 0; //上は配列の長さでスタート var high = array.Length; //highがlow以下になるまで探索を繰り返す while (low < high) { //中間地点(index) var middle = (high + low) / 2; //中間地点よりもvalueが小さければ下を中間地点に引き下げる if (value < array[middle]) high = middle; //valueが中間地点以上であれば下を中間地点に引き上げる else low = middle + 1; } return low; }
まず引数は
long value = Bi
long[] array = Ciの配列
です。
呼び出し元では
Nから返ってきた値を引いたものを
Biよりも大きいCiの個数として扱っています。
そのため、
このメソッドから返しているのはindexなのに
呼び出し元ではBi以下の個数として扱われているので
lowに入っているindexは
Biよりも一つ大きいほうにずれたものであることが分かります。
また先の
Biよりも小さいAiの個数を求めるメソッドとは
条件式の等符号が入っている位置が違っています。
これは欲しい個数が異なるものであることに依る
違いだと思われます。
例えば
AiもCiも以下の同じ配列であるとします。
1 2 3 4 5 6 7
このときBiが「4」だとすると
Aiの個数を求めるメソッドは
4より小さい数の個数3を返しますが
(実際にはindexなのでlowが指しているのは4(low=valueのパターン))
Ciの個数を求めるメソッドは
Bi以下の値の個数を返すので4を返し、
(実際にはindexなのでlowが指しているのは5(valueよりもlowが一つ大きいパターン))
Biよりも大きい値は7-4=3と求めることができます。
全体のコード
using System; using System.Linq; using System.Collections.Generic; class Cmondai { static void Main() { var N = Int32.Parse(Console.ReadLine()); //値が小さい順に並び替え var A = Console.ReadLine().Split(' ').Select(x => Int64.Parse(x)).OrderBy(x => x).ToArray(); var B = Console.ReadLine().Split(' ').Select(x => Int64.Parse(x)).OrderBy(x => x).ToArray(); var C = Console.ReadLine().Split(' ').Select(x => Int64.Parse(x)).OrderBy(x => x).ToArray(); //答えを格納する変数 var ans = 0L; //Biに対する Biよりも小さいAiの個数と Biよりも大きいCiの個数を求めて //各Biに対する祭壇の種類数を出して 足していく //各Biについて種類数を出したいのでN回まわす for (int i = 0; i < N; ++i) { //Biよりも小さいのAiの個数 var a = LowerBound(B[i], A); //Bi以下のCiの個数 var c = UpperBound(B[i], C); //各Biに対する祭壇の種類数を出して 足す ans += a * (N - c); } Console.WriteLine(ans); } //引数 value=Bi array=Ciの配列 static long UpperBound(long value, long[] array) { //下は0でスタート var low = 0; //上は配列の長さでスタート var high = array.Length; //highがlow以下になるまで探索を繰り返す while (low < high) { //中間地点(index) var middle = (high + low) / 2; //中間地点よりもvalueが小さければ下を中間地点に引き下げる if (value < array[middle]) high = middle; //valueが中間地点以上であれば下を中間地点に引き上げる else low = middle + 1; } return low; } //引数 value=Bi array=Aiの配列 static long LowerBound(long value, long[] array) { //下は0でスタート var low = 0; //上は配列の長さでスタート var high = array.Length; //highがlow以下になるまで探索を繰り返す while (low < high) { //中間地点(index) var middle = (high + low) / 2; //valueが中間地点以下であれば上を中間地点に引き下げる if (value <= array[middle]) high = middle; //中間地点よりもvalueが大きければ下を中間地点に引き上げる else low = middle + 1; } return low; } }