教育的ないい問題!!!!!
問題概要
本の棒があって、それぞれ
の長さをもっている。
このうちの 3 本を選ぶ方法であって、その 3 本で三角形が作れるものは何通あるか?
制約
解法の overview
ものすごく色んな解法が考えられる問題だと思う。まずナイーブには 通りの選び方を全探索する方法が考えられる。しかし
かかって間に合わない
(上手く枝刈りすると間に合ってしまったらしいが...)
そこで、こういうときによく考えるのが
3 つのうち、2 つを固定して考える
というあたり。とりあえず三角形の三辺を短い順に (
) として、
を固定
を固定
といったあたりが思いつく。どちらでやっても解ける。ここでは を固定してやってみる。
解法 1: 二分探索
のうちの
を固定すると、残りの辺
の満たすべき条件は
となる。下図のような感じ。 のとき、
なので、
のとりうる範囲は、下図のように
より右側
未満の領域
ということになる。
ここで、 のうち「最初に
以上になってしまうインデックス」は二分探索で求めることができる。lower_bound() とか使えば OK。
まとめると
を全探索
の中で最初に
以上になってしまうインデックスを求める
の動ける範囲がそれによって求まるので、合計していく
という風に解ける。 の固定方法が
だけあって、それに応じて
の動ける範囲を求めるのは二分探索で
でできるので、全体の計算量は
となる。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int N; cin >> N; vector<long long> L(N); for (int i = 0; i < N; ++i) cin >> L[i]; sort(L.begin(), L.end()); // L をソートしておく (二分探索に必要) long long res = 0; // a と b を固定 for (int i = 0; i < N; ++i) { for (int j = i+1; j < N; ++j) { // c の動ける範囲の右端を求める int k = lower_bound(L.begin(), L.end(), L[i] + L[j]) - L.begin(); // c の動ける範囲は、[j+1, k) res += max(k - (j+1), 0); } } cout << res << endl; }
解法 2: しゃくとり法
解法 1 では を固定した。そこでは
を固定して
の動く範囲の右端 (
の中で最初に
以上になってしまう場所) を求める
という風にしていた。ここでもっと突き詰めて
を固定する
を動かしていくと、
の右端が右へと単調にズレていく
ということがわかるので、しゃくとり法を使うことができる。
つまり、初期設定として、b を a の場所に、c を b の場所に設定しておいて、
- b を右に動かす
- c >= a + b になるまで c を右に動かす
をくりかえせば OK。計算量は各 に対して
でできるので、全体として
となる。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { int N; cin >> N; vector<long long> L(N); for (int i = 0; i < N; ++i) cin >> L[i]; sort(L.begin(), L.end()); // L をソートしておく long long res = 0; // a を固定して、(b, c) をしゃくとり法 for (int i = 0; i < N; ++i) { int k = i; for (int j = i+1; j < N; ++j) { while (k < N && L[k] < L[i]+L[j]) ++k; res += k - (j+1); } } cout << res << endl; }
解法 3: L が小さいことを活かして累積和
今回の問題は の制約がとても小さいことがわかる。こういうときに、たとえば
とかに対して
とかにするのをよくやる。C[ i ] は、L の中に i が何個あるのかを表す。これのメリットは、
L の中で x 以上 y 未満の値が何個あるのかが、累積和を使うと簡単にわかる。
具体的には C の累積和を S とすると、S[ y ] - S[ x ] で求められる
というもの。そして解法 1, 2 と違って、 を仮定せずに求めて、最後に 3! = 6 で割ることにする。
一般に三角形の三辺 に対して三角形になるための必要十分条件は、
と表せる。ここでは を仮定していないことに注意。これを利用して
を固定する
- そうすると
の満たすべき条件は
と簡単に表せるので、これを満たす
の個数は、累積和を使って S[
] - S[ abs(
) +
] と求められる!!!
- ただし、この
の範囲に
や
も含まれていたらその分を減らす
- ただし、この
という風にすれば OK。計算量は、累積和計算に 、その後の処理に
となる。
#include <iostream> #include <vector> #include <cmath> #include <algorithm> using namespace std; const int MAX = 2100; int main() { long long res = 0; int N; cin >> N; vector<long long> L(N); for (int i = 0; i < N; ++i) cin >> L[i]; // C, S vector<long long> C(MAX, 0), S(MAX+1, 0); for (int i = 0; i < N; ++i) C[L[i]]++; for (int x = 0; x < MAX; ++x) S[x+1] = S[x] + C[x]; // 累積和 // a と b を固定 for (int i = 0; i < N; ++i) { for (int j = 0; j < N; ++j) { if (i == j) continue; // 同じものは選ばない // c の満たすべき値の区間を [left, right) で表す int left = abs(L[i] - L[j]) + 1; int right = L[i] + L[j]; // 累積和で個数を求める int count = S[right] - S[left]; if (left <= L[i] && L[i] < right) --count; if (left <= L[j] && L[j] < right) --count; res += max(count, 0); } } cout << res/6 << endl; }