C++で解く整数置換問題:1になるまでの最小操作回数を求めるアルゴリズム
正の整数 n が与えられ、これに対して次の操作を繰り返し適用できるものとします。
nが偶数の場合:nをn / 2に置き換えるnが奇数の場合:nをn + 1またはn - 1のいずれかに置き換える
このとき、n を 1 にするまでに必要な最小の操作回数を求めるのが本問題です。
問題の例
たとえば n = 7 の場合、答えは 4 になります。次のような操作列が存在するからです。
- 7 → 8 → 4 → 2 → 1
- 7 → 6 → 3 → 2 → 1
解法のアプローチ(貪欲法)
この問題は、以下の手順で貪欲に解くことができます。
ret := 0、n := xと初期化するn > 1の間、次を繰り返すnが偶数なら、n := n / 2とするnが奇数なら、n == 3またはn / 2が偶数(つまりn % 4 == 1)の場合はnを 1 減らし、それ以外の場合は 1 増やすretを 1 増やす
- 最後に
retを返す
なぜこの戦略が有効か
n が奇数で n % 4 == 3 を満たすとき、n + 1 とすると 4 の倍数になるため、2 回連続で 2 による除算が可能になり、結果として少ない操作で済みます。一方、n == 3 の場合は例外で、3 → 2 → 1(2 回)とする方が 3 → 4 → 2 → 1(3 回)よりも短いため、必ず減算を選びます。また、オーバーフロー防止のため n は long long 型で扱っている点にも注目してください(n = INT_MAX のとき n + 1 が発生する可能性があるためです)。
C++での実装例
以下のコードで、実際の動作を確認してみましょう。
#include <bits/stdc++.h>
using namespace std;
typedef long long int lli;
class Solution {
public:
int bitCount(int x){
int ret = 0;
while(x){
ret++;
x >>= 1;
}
return ret;
}
int integerReplacement(int x) {
int ret = 0;
lli n = x;
while(n > 1){
if(n % 2 == 0){
n >>= 1;
}
else if(n & 1){
if(n == 3 || (((n >> 1) & 1 )== 0)){
n--;
} else {
n++;
}
}
ret++;
}
return ret;
}
};
main(){
Solution ob;
cout << (ob.integerReplacement(7));
}入力
7
出力
4
まとめ
本アルゴリズムでは、各ステップで n を半分以下に減らしていくため、計算量は O(log n) となります。ビット演算を活用した判定により、シンプルかつ高速に最小操作回数を求められるのがポイントです。
-
C++で学ぶ符号なし整数のリストアリング除算アルゴリズム
本記事では、除算アルゴリズムを用いて符号なし整数の割り算を行う方法について解説します。除算アルゴリズムには、紙の上で手計算として行われるものと、デジタル回路に実装されるものがあります。除算アルゴリズムは大きく「低速除算アルゴリズム」と「高速除算アルゴリズム」の2種類に分類され、低速除算アルゴリズムにはリストアリング法、非実行リストアリング法、SRT法、非リストアリング法などが含まれます。 このチュートリアルでは、0 < 除数 < 被除数 を前提として、リストアリング(Restoring)除算アルゴリズムについて詳しく見ていきます。 解法のアプローチ ここでは、商を格納するレジスタQ
-
C++で整数の各桁を拡大表示(ズーム)するプログラムの作り方
この記事では、C++を使って整数の各桁を拡大表示(ズーム)するプログラムを紹介します。ここでの「ズーム」とは、数字を「#」などの記号を使って通常より大きな形で描画することを指します。考え方自体はシンプルですが、0から9までの各数字を大きな形式で一つずつパターン化する必要があります。 ズーム表示の基本的な考え方 各数字は5行×5列のグリッドとして表現します。二重のforループでグリッド全体を走査しながら、行・列の位置関係をもとに条件分岐を行い、「#」と空白のどちらを出力するかを決めます。たとえば「0」なら外枠だけを「#」で囲み、「8」なら上下の横線と左右の縦線を組み合わせて描画するといった具合で