C++で配列内のk個の最強値を求めるアルゴリズム
問題の概要
数値の配列 arr と整数 k が与えられているとします。ここで、値 arr[i] が値 arr[j] より「強い(strong)」とは、次の条件を満たすことを指します。
|arr[i] − m| > |arr[j] − m|(m は配列の中央値)
また、|arr[i] − m| と |arr[j] − m| が等しい場合は、arr[i] > arr[j] であるときに arr[i] の方が強いとみなされます。このルールに基づき、配列の中から最も強い k 個の値をリストとして返すのが本問題の目的です。
具体例
入力が arr = [1,2,3,4,5]、k = 2 の場合を考えてみましょう。このとき中央値は 3 であり、強さの順に要素を並べ替えると [5, 1, 4, 2, 3] となります。したがって、最も強い 2 つの要素は [5, 1](または [1, 5])です。
|5 − 3| と |1 − 3| はどちらも 2 で等しいのですが、5 > 1 であるため、5 の方が 1 より強いと判定されます。
解法のアプローチ
この問題は、ソート + 双方向ポインタ(ツーポインタ)のテクニックを使うことで効率的に解けます。手順は以下の通りです。
配列 arr をソートする
n := arr のサイズとする
m := arr[(n − 1) / 2](ソート後の中央値)
結果格納用の配列 ret を定義する
i := 0(先頭ポインタ)、j := n − 1(末尾ポインタ)とする
k が 0 になるまで以下を繰り返す:
x1 := |arr[j] − m|(末尾側の中央値からの距離)
x2 := |arr[i] − m|(先頭側の中央値からの距離)
x1 ≥ x2 の場合:ret の末尾に arr[j] を追加し、j を 1 減らす
それ以外の場合:ret の末尾に arr[i] を追加し、i を 1 増やす
ret を返す
ソート済みの配列では、中央値から最も遠い候補は必ず先頭か末尾に存在します。そのため、両端を比較しながら強い方を取り出していくこの方法で、毎回正しく最強の値を選択できます。計算量はソートに O(n log n)、抽出処理に O(k) となり、非常に効率的です。
C++での実装例
以下のコードで実際の動作を確認できます。
#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<int> v){
cout << "[";
for(int i = 0; i<v.size(); i++){
cout << v[i] << ", ";
}
cout << "]"<<endl;
}
class Solution {
public:
int calc(int x, int m){
return abs(x - m);
}
vector<int> getStrongest(vector<int>& arr, int k) {
sort(arr.begin(), arr.end());
int n = arr.size();
int m = arr[(n - 1) / 2];
vector<pair<int, int> > v;
int i = 0;
int j = n - 1;
vector<int> ret;
while (k--) {
int x1 = calc(arr[j], m);
int x2 = calc(arr[i], m);
if (x1 >= x2) {
ret.push_back(arr[j]);
j--;
}
else {
ret.push_back(arr[i]);
i++;
}
}
return ret;
}
};
main(){
Solution ob;
vector<int> v = {1,2,3,4,5};
print_vector(ob.getStrongest(v,2));
}入力
{1,2,3,4,5},2出力
[5, 1]
まとめ
本記事では、中央値からの距離と値の大小を基準に「強さ」を判定し、配列から最強の k 個の値を抽出する方法を紹介しました。ポイントは、配列をソートした上で両端から比較を進める双方向ポインタの活用です。同点の場合は大きい値を優先するという条件(x1 ≥ x2 の分岐)を正しく実装することが、正解への鍵となります。
-
C++で解く「Maze III」:ボールを最短距離で穴に落とすアルゴリズム
問題の概要 空きスペースと壁からなる迷路の中に、ボールが1つ置かれています。ボールは空きスペース上を上(u)・下(d)・左(l)・右(r)のいずれかの方向に転がって移動できますが、壁にぶつかるまで停止しません。ボールが停止した時点で、次の方向を選択できます。また、迷路内には穴(hole)が1つあり、ボールが穴の位置まで転がると、その穴に落ちます。 ボールの初期位置・穴の位置・迷路の情報が与えられたとき、ボールを最短距離で穴に落とすための移動手順を求めます。ここでいう距離とは、スタート地点(含まない)から穴(含む)までにボールが通過した空きスペースの数として定義されます。 移動方向は「u」「d
-
【C++入門】配列を関数に渡す3つの方法をわかりやすく解説
C++では、配列全体をそのまま関数の引数として渡すことはできません。しかし、インデックスを付けずに配列名を指定することで、配列へのポインタを渡すことができます。これは「配列名は先頭要素へのポインタに読み替えられる(配列の減衰)」というC++の仕組みによるものです。1次元配列を関数の引数として渡したい場合は、以下の3つのいずれかの方法で関数の仮引数を宣言します。どの方法でも、コンパイラに対して「整数型のポインタを受け取る」という情報が伝わるため、動作結果はすべて同じになります。配列を関数に渡す3つの宣言方法1. ポインタとして仮引数を宣言するvoid myFunction(int *param)