E - 会社経営シミュレーション / Company Management Simulation Editorial by admin
Gemini 3.0 Flash (Thinking)概要
\(N\) 日周期で繰り返される経営計画において、指定された開始日 \(L\) と初期資金 \(S\) からスタートしたとき、いつ初めて資金が負になるか(倒産するか)を判定する問題です。資金の減少が続く場合は、非常に長い期間を経て倒産する可能性があるため、シミュレーションではなく数学的な計算とデータ構造を用いて高速に解く必要があります。
考察
1. 資金の変化を数式で表す
\(i\) 日目の資金の増減を \(D_i = A_i - B_i - C_i\) とします。 経営開始から \(k\) 日目の終了時点での資金は、以下のように表せます。 $\(\text{資金} = S + \sum_{d=1}^{k} D_{\text{(開始から } d \text{ 日目の計画上の日付)}}\)$
この和の部分を高速に計算するために、累積和を利用します。周期性を扱うため、計画 \(2\) サイクル分(\(2N\) 日分)の累積和 \(P_i\) を用意しておくと、どの開始日 \(L\) からの \(N\) 日間の変化も \(P_{L+r-1} - P_{L-2}\) のような形で表現できます。
2. 倒産するかどうかの判定
1サイクル(\(N\) 日間)の合計収支を \(T = \sum_{i=1}^N D_i\) とします。
- \(T \geq 0\) の場合: 最初の \(N\) 日間(1サイクル目)を乗り切れば、その後は資金が減らないか増えていくため、二度と倒産しません。
- \(T < 0\) の場合: 1サイクルごとに資金が \(|T|\) ずつ減っていくため、いつかは必ず倒産します。
3. 倒産するタイミングの特定
倒産が「何サイクル目」の「何日目」に起こるかを切り分けて考えます。
- 最初の \(N\) 日間で倒産する場合: 最初のサイクル内での最小資金(相対値)を \(M\) としたとき、\(S + M < 0\) ならばこのサイクル中に倒産します。
- それ以降のサイクルで倒産する場合: \(T < 0\) かつ最初のサイクルを乗り切った場合、何サイクルか経過した後に倒産します。 \(q\) サイクル経過後の資金の推移は、最初のサイクルの推移を \(q \times T\) だけ平行移動させたものになります。 「\(q\) サイクル目までは耐えられるが、\(q+1\) サイクル目で倒産する」という \(q\) は、不等式 \(S + qT + M < 0\) を解くことで \(q = \lfloor (S+M)/|T| \rfloor + 1\) のように求められます。
具体的な「日」を特定するには、累積和の配列に対して「ある値より小さくなる最初のインデックス」を探す必要があります。これはセグメント木を用いることで、1クエリあたり \(O(\log N)\) で実行可能です。
アルゴリズム
- 前処理:
- 各日の収支 \(D_i\) を計算し、2サイクル分(長さ \(2N\))の累積和 \(P\) を作成する。
- \(P\) をもとに、「区間の最小値」および「区間内で値が \(V\) 未満となる最初の位置」を取得できるセグメント木を構築する。
- クエリ処理:
- 開始日 \(L\) から \(N\) 日間での最小相対変化 \(M\) をセグメント木で求める。
- \(S + M \geq 0\) かつ \(T \geq 0\) ならば、倒産しないため
0を出力。 - そうでない場合、倒産するサイクル \(q\) を計算する(\(S + M < 0\) なら \(q=0\))。
- \(q\) サイクル後の資金状況において、初めて資金が負になる日をセグメント木の
find_first(二分探索)を用いて特定する。
計算量
- 時間計算量: \(O((N + Q) \log N)\)
- セグメント木の構築に \(O(N)\)。
- 各クエリに対し、最小値取得と二分探索を \(O(\log N)\) で行うため、全体で \(O(Q \log N)\)。
- 空間計算量: \(O(N)\)
- 累積和配列とセグメント木に \(O(N)\) のメモリを使用します。
実装のポイント
周期の扱い: 配列を \(2N\) で持つことで、開始日 \(L\) がどこであっても \(L\) 日目から \(L+N-1\) 日目までの連続した区間として扱えるようになり、実装が単純になります。
セグメント木上の二分探索: 「値が \(V\) 未満となる最初の要素」を探す際、単純な二分探索をセグメント木の外で行うと \(O(\log^2 N)\) になりますが、セグメント木のノードを辿る形で行えば \(O(\log N)\) で済みます。
オーバーフロー: 累積和や初期資金 \(S\) は最大で \(10^{12}\) 以上、計算過程ではそれ以上の値になる可能性があるため、
long long型(64bit整数)を使用する必要があります。ソースコード
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
/**
* Segment Tree to find the minimum value in a range and the first index satisfying a condition.
* This will help in determining the first day the funds drop below zero.
*/
struct SegmentTree {
int n;
vector<long long> tree;
static constexpr long long INF = 2e18; // Larger than any possible prefix sum
SegmentTree(const vector<long long>& data) {
n = (int)data.size();
tree.resize(4 * n);
build(data, 1, 0, n - 1);
}
// Build the segment tree with minimum values in each range.
void build(const vector<long long>& data, int node, int start, int end) {
if (start == end) {
tree[node] = data[start];
return;
}
int mid = start + (end - start) / 2;
build(data, 2 * node, start, mid);
build(data, 2 * node + 1, mid + 1, end);
tree[node] = min(tree[2 * node], tree[2 * node + 1]);
}
// Range minimum query to find the lowest fund level in a cycle.
long long query_min(int node, int start, int end, int L, int R) {
if (R < start || end < L) return INF;
if (L <= start && end <= R) return tree[node];
int mid = start + (end - start) / 2;
return min(query_min(2 * node, start, mid, L, R),
query_min(2 * node + 1, mid + 1, end, L, R));
}
/**
* Find the first index in [L, R] such that data[index] < V.
* This is used to find the specific day of bankruptcy.
*/
int find_first(int node, int start, int end, int L, int R, long long V) {
// If the range is invalid or the minimum in this range is not less than V, return -1.
if (R < start || end < L || tree[node] >= V) return -1;
if (start == end) return start;
int mid = start + (end - start) / 2;
int res = find_first(2 * node, start, mid, L, R, V);
if (res == -1) {
res = find_first(2 * node + 1, mid + 1, end, L, R, V);
}
return res;
}
};
int main() {
// Fast I/O for competitive programming performance.
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
if (!(cin >> N >> Q)) return 0;
// Daily net change in funds: D_i = A_i - B_i - C_i
vector<long long> D(N);
for (int i = 0; i < N; ++i) {
long long A, B, C;
cin >> A >> B >> C;
D[i] = A - B - C;
}
// Prefix sums for two cycles (2N days) to handle the wrap-around logic easily.
// P_data[i] stores the sum of D from day 1 to day i+1.
vector<long long> P_data(2 * N);
long long current_P = 0;
for (int i = 0; i < 2 * N; ++i) {
current_P += D[i % N];
P_data[i] = current_P;
}
// Total fund change over one full cycle of N days.
long long T = P_data[N - 1];
// Build the segment tree on the prefix sums of the extended sequence.
SegmentTree st(P_data);
for (int j = 0; j < Q; ++j) {
int L;
long long S;
cin >> L >> S;
// P_prev is the prefix sum before starting on day L.
long long P_prev = (L == 1) ? 0 : P_data[L - 2];
// M is the minimum relative change in funds within the first N days starting from L.
// The funds after r days starting from L are S + P_{L+r-1} - P_{L-1}.
long long M = st.query_min(1, 0, 2 * N - 1, L - 1, L + N - 2) - P_prev;
if (S + M >= 0) {
// If the company survives the first full cycle starting from day L.
if (T >= 0) {
// If the net cycle change is non-negative, it will never bankrupt.
cout << 0 << "\n";
} else {
// If the net cycle change is negative, it will eventually bankrupt.
long long absT = -T;
// Calculate how many full cycles q it takes before it enters a cycle where it bankrupts.
// Bankruptcy condition: S + q*T + M < 0 => q*|T| > S + M
long long q = (S + M) / absT + 1;
long long S_prime = S + q * T;
long long V = P_prev - S_prime;
// Find the first day within the (q+1)-th cycle where bankruptcy happens.
int idx = st.find_first(1, 0, 2 * N - 1, L - 1, L + N - 2, V);
long long r = (long long)idx - L + 2;
cout << (long long)q * N + r << "\n";
}
} else {
// Bankruptcy occurs within the first cycle of N days.
long long V = P_prev - S;
int idx = st.find_first(1, 0, 2 * N - 1, L - 1, L + N - 2, V);
long long r = (long long)idx - L + 2;
cout << r << "\n";
}
}
return 0;
}
この解説は gemini-3-flash-thinking によって生成されました。
posted:
last update: