C - バッテリー残量と省エネモード / Battery Level and Power Saving Mode Editorial by admin
(非推奨) gemini-3.5-flash-thinking概要
ノートパソコンのバッテリー消費を管理しながら、\(N\) 個のタスクを順番に処理する問題です。 フルパワーモードを使用するたびにバッテリー残量が半分になるという性質を利用し、動的計画法(DP)を用いてフルパワーモードの最小使用回数を求めます。
考察
重要な気づき:フルパワーモードを使用できる回数の上限
初期バッテリー残量 \(M\) は最大で \(10^{18}\) と非常に大きな値ですが、フルパワーモードを使用するたびにバッテリー残量は \(\lfloor p/2 \rfloor\) と半分になっていきます。
\(10^{18}\) を 2 で割り続けると、約 60 回でバッテリー残量は 0 になります。 したがって、フルパワーモードを使用できる回数は高々 60 回程度(正確には \(\log_2 M + 1\) 回以下)しかありません。この「状態数が非常に少ない」という性質が、この問題を解くための最大の鍵となります。
なぜ貪欲法では解けないか?
「必要処理能力 \(H_i\) が \(A\) を超えたときだけフルパワーを使う」というような単純な貪欲法では解けません。 なぜなら、現在のタスクを省エネモードでクリアできる場合(\(H_i \le A\))であっても、将来のより厳しいタスクに備えて、あえて今のうちにフルパワーモードを使っておく(あるいはバッテリーを温存しておく)といった判断が必要になる場合があるからです。
どのタイミングでフルパワーを使うべきかは、先のタスクの必要処理能力に依存するため、すべての可能性を探索する必要があります。
DP(動的計画法)によるアプローチ
フルパワーモードの最大使用回数を \(L\)(\(\approx 60\))とします。 タスクを 1 番目から順に見ていきながら、以下の情報を管理するDPを考えます。
dp[k]: 現在までにフルパワーモードをちょうど \(k\) 回使用した状態に到達可能か(true/false)
タスクを進めるごとに、この dp 配列を更新していきます。状態数が \(L \le 60\) と非常に小さいため、タスク数 \(N\) に対しても高速に動作します。
アルゴリズム
1. 前処理
フルパワーモードを \(k\) 回使った後のバッテリー残量を表す配列 \(P\) を作成します。
- P[0] = M から開始し、curr /= 2 を curr > 0 である限り繰り返して配列に追加します。
- 配列のサイズを \(L + 1\) とします(\(P[L] = 0\) となります)。
2. DPの遷移
各タスク \(i = 0, 1, \ldots, N-1\) について、現在の dp から次のタスク終了時の状態 next_dp を遷移させます。
dp[k] が true(到達可能)であるとき、以下の 2 つの選択肢があります。
省エネモードを使用する
- タスクの必要処理能力が \(A\) 以下(\(H_i \le A\))であれば、バッテリーを消費せずにクリアできます。
- このとき、フルパワー使用回数は \(k\) 回のままなので、
next_dp[k] = trueとします。
フルパワーモードを使用する
- まだバッテリーが残っており(\(k < L\))、かつ現在のバッテリー残量 \(P[k]\) が \(H_i\) 以上(\(P[k] \ge H_i\))であれば、フルパワーモードでクリアできます。
- このとき、フルパワー使用回数は \(k+1\) 回になるので、
next_dp[k+1] = trueとします。
各タスクの処理が終わるたびに、dp を next_dp で更新します。
3. 答えの出力
すべてのタスクを処理し終えた後、dp[k] が true となっている最小の \(k\)(\(0 \le k \le L\))が答えとなります。
どの \(k\) に対しても dp[k] が false である場合は、すべてのタスクを完了することは不可能なので -1 を出力します。
計算量
時間計算量: \(O(N \log M)\)
- バッテリーが 0 になるまでの回数 \(L\) は \(\log_2 M\) に比例します(\(M \le 10^{18}\) のとき \(L \le 60\))。
- 各タスクに対して、サイズ \(L\) のループを回して遷移を行うため、全体の計算量は \(O(N \log M)\) となります。
- \(N = 3 \times 10^5\) のとき、全体のループ回数は最大でも約 \(1.8 \times 10^7\) 回であり、実行時間制限に十分間に合います。
空間計算量: \(O(N + \log M)\)
- 入力されたタスクの配列 \(H\) を保持するのに \(O(N)\) のメモリを使用します。
- DPテーブルは現在の状態と次の状態の 2 世代分(サイズ \(L \approx 60\))だけ保持すればよいため、DPに必要な空間は \(O(\log M)\) です。
実装のポイント
データ型: バッテリー残量 \(M\) や必要処理能力 \(H_i\) は最大 \(10^{18}\) に達するため、C++では 64 ビット整数型(
long long)を使用する必要があります。メモリの節約: DPテーブルを遷移させる際、
std::moveを使用してnext_dpをdpに代入することで、不要なメモリコピーを避けて高速化を図っています。ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
// 標準入出力の高速化
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N;
long long M, A;
if (!(cin >> N >> M >> A)) return 0;
vector<long long> H(N);
for (int i = 0; i < N; ++i) {
cin >> H[i];
}
// P[k] はフルパワーモードを k 回使った後のバッテリー残量
vector<long long> P;
long long curr = M;
P.push_back(curr);
while (curr > 0) {
curr /= 2;
P.push_back(curr);
}
int L = P.size() - 1; // P[L] = 0 となる
// dp[k] : 現在、フルパワーモードを k 回使った状態に到達可能か (0: 不可能, 1: 可能)
vector<char> dp(L + 1, 0);
dp[0] = 1;
for (int i = 0; i < N; ++i) {
vector<char> next_dp(L + 1, 0);
long long hi = H[i];
for (int k = 0; k <= L; ++k) {
if (!dp[k]) continue;
// 省エネモードを使用する場合
if (hi <= A) {
next_dp[k] = 1;
}
// フルパワーモードを使用する場合
if (k < L && P[k] >= hi) {
next_dp[k + 1] = 1;
}
}
dp = move(next_dp);
}
int ans = -1;
for (int k = 0; k <= L; ++k) {
if (dp[k]) {
ans = k;
break;
}
}
cout << ans << "\n";
return 0;
}
この解説は gemini-3.5-flash-thinking によって生成されました。