C++で解く「石を動かして連続配置にする II」― 移動回数の最小値と最大値を求める
問題概要
無限に続く数直線を考えます。i 番目の石の位置は配列 stones で与えられ、stones[i] が i 番目の石の座標を表します。最も小さい位置、または最も大きい位置にある石を「端点の石」と呼びます。各ターンでは、端点の石を1つ選び、それ以上端点にならないように、まだ占有されていない空き位置へ移動させます。
例えば stones = [1,2,5] の場合、位置 5 にある端点の石は動かせません。0 や 3 など、どの空き位置に置いてもその石は再び端点になってしまうためです。
この操作を繰り返し、すべての石が連続した位置に並んでこれ以上手が打てなくなった時点で、ゲームは終了します。
求めたいのは、ゲーム終了までに行われる移動回数の最小値と最大値です。答えは [min_moves, max_moves] のペアとして返します。
例えば入力が [7,3,9] なら、出力は [1,3] となります。
解法のアプローチ
以下の手順でこの問題を解いていきます。
サイズ2の配列
ansを用意する。ans[0]に無限大、ans[1]に−無限大、nに配列aの要素数を代入する。配列
aを昇順にソートする。x := 1とし、x < nかつa[x] − a[x−1] = 1である間xを増加させる(先頭から何個の石が連続しているかを調べる)。x == nなら、すべての石が既に連続しているため{0, 0}を返す。minVal := 0、j := 1と初期化する。i を 0 から順に走査し、各 i について次の処理を行う。
curr := a[i]、lastPossible := a[i] + n − 1とする。lastPossible > a[n−1]ならループを抜ける。spaceInBetween := falseとする(窓の中に隙間があるかどうかを示すフラグ)。j <= iならj := i + 1に更新する。j < nかつa[j] <= lastPossibleの間、隣接する石の間隔が1より大きければspaceInBetween := trueとし、j を1ずつ進める。idx := j − 1とする。窓の外に2個以上の石が残る場合(
n − (idx − i + 1) > 1)もspaceInBetween := trueとする。ballLeft := i、ballRight := n − (idx + 1)とし、minVal = ballLeft + ballRight + (spaceInBetween ? 0 : 1)を計算する。ans[0] = min(ans[0], minVal)で最小移動回数を更新する。
ans[1] = max(a[n−2] − a[0], a[n−1] − a[1]) − (n − 2)として最大移動回数を求める。ansを返す。main 関数からはsolve(stones)を呼び出します。
ポイント解説
最小移動回数は、長さ n の「窓」を数直線上でスライドさせ、窓内に収まる石の数が最大になる位置を探すことで求められます。窓の外に残った石を窓内の空きマスへ1つずつ移す回数が基本となり、窓内に隙間がない場合はさらに1手余分に必要になります。
最大移動回数は、「どちらか一方の端の石のみを固定し、残りの n−2 個の石をすべて詰め直す」戦略で達成できます。これが max(a[n−2] − a[0], a[n−1] − a[1]) − (n − 2) という式に対応します。
C++ 実装例
#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<auto> v){
cout << "[";
for(int i = 0; i<v.size(); i++){
cout << v[i] << ", ";
}
cout << "]"<<endl;
}
class Solution {
public:
vector<int> solve(vector<int> a) {
vector <int> ans(2);
ans[0] = INT_MAX;
ans[1] = INT_MIN;
int n = a.size();
sort(a.begin(), a.end());
int x = 1;
while(x < n && a[x] - a[x - 1] == 1)
x ++;
if(x == n){
return {0,0};
}
int minVal = 0;
int j = 1;
for(int i = 0; i < a.size(); i++){
int curr = a[i];
int lastPossible = a[i] + n - 1;
if(lastPossible > a[n - 1])
break;
bool spaceInBetween = false;
if(j <= i)
j = i + 1;
while(j < n && a[j] <= lastPossible){
if((a[j] - a[j - 1]) > 1) {
spaceInBetween = true;
}
j++;
}
int idx = j - 1;
if(n - (idx - i + 1) > 1)
spaceInBetween = true;
int ballLeft = i;
int ballRight = n - (idx + 1);
minVal = ballLeft + ballRight + (spaceInBetween? 0 : 1);
ans[0] = min(minVal, ans[0]);
}
ans[1] = max(a[n - 2] - a[0], a[n - 1] - a[1]) - (n -2);
return ans;
}
vector<int> numMovesStonesII(vector<int>& stones) {
return solve(stones);
}
};
main(){
Solution ob;
vector<int> v1 = {7,3,9};
print_vector(ob.numMovesStonesII(v1));
}
入力
[7,3,9]
出力
[1, 3]
-
C++で解くMax Consecutive Ones II:0を1回反転できる場合の最大連続1数の求め方
0と1のみから構成されるバイナリ配列が与えられたとき、「0を最大1回だけ反転できる」という条件下で、配列内に存在する連続した1の最大個数を求める問題について解説します。 例えば、入力が [1,0,1,1,0] の場合、出力は 4 となります。最初に出現する0を反転すれば [1,1,1,1,0] となり、先頭から4つ連続した1が得られるためです。 解法の考え方:スライディングウィンドウ この問題は、スライディングウィンドウ(尺取り法)を用いることで、線形時間で効率的に解くことができます。ウィンドウ内に含まれる0の個数が「1個以内」に収まるように、右端を伸ばしながら左端を調整していくのがポイントで
-
【C++】二分木における最長連続シーケンス経路の求め方を解説
問題の概要二分木が与えられたとき、最長の連続シーケンス経路の長さを求める問題を考えます。ここで「経路」とは、ある開始ノードから親子のつながり(親から子へのエッジ)に沿って、木の中の任意のノードまでをたどるノードの列を指します。最長の連続経路は必ず親から子の方向へ進む必要があり、逆方向(子から親)へさかのぼることは認められません。たとえば、次のような二分木が入力として与えられた場合を考えてみましょう。この場合、最長の連続シーケンス経路は 3 → 4 → 5 となるため、出力は 3 になります。アルゴリズムのアプローチこの問題は、木を深さ優先探索(DFS)でたどりながら、連続する値の並びを追跡する