E - 会社経営シミュレーション / Company Management Simulation 解説 by admin
gpt-5.5-xhigh概要
各日の資金増減を \(d_i=A_i-B_i-C_i\) として、周期的に繰り返される数列上で「累積資金が初めて負になる日」を各質問ごとに求めます。
累積和を \(2N\) 日分用意し、区間最小値と「初めてある値未満になる位置」をセグメント木で高速に求めます。
考察
まず、各日の資金増減を
\[ d_i = A_i - B_i - C_i \]
とします。
経営計画は周期 \(N\) で繰り返されるので、\(d_1,d_2,\ldots,d_N\) が無限に繰り返される数列だと考えられます。
質問で \(L\) 日目から始める場合、最初の \(N\) 日間は
\[ L,L+1,\ldots,N,1,2,\ldots,L-1 \]
という順番になります。
このような「途中から始まって一周する区間」を扱いやすくするために、数列を \(2\) 回並べた累積和を考えます。
累積和を
\[ p_0=0 \]
\[ p_i=p_{i-1}+d_{((i-1)\bmod N)+1} \]
とします。
すると、\(L\) 日目から始めたとき、経営開始から \(t\) 日後 \((1 \leq t \leq N)\) の資金増減は
\[ p_{L+t-1} - p_{L-1} \]
で表せます。
初期資金が \(S\) 円なので、倒産条件は
\[ S + p_{L+t-1} - p_{L-1} < 0 \]
です。
これを変形すると、
\[ p_{L+t-1} < p_{L-1} - S \]
となります。
つまり、区間 \([L, L+N-1]\) の中で、初めて累積和がある値未満になる位置を探せばよいです。
ただし、累積和は単調とは限らないため、単純な二分探索はできません。
また、各質問ごとに \(N\) 日分を調べると \(O(NQ)\) となり、最大で間に合いません。
そこで、セグメント木を使って次の操作を高速に行います。
- 区間の最小値を求める
- 区間内で初めて \(x\) 未満になる位置を求める
次に、\(1\) サイクル全体の増減を
\[ T = d_1+d_2+\cdots+d_N \]
とします。
\(T \geq 0\) の場合
\(1\) サイクル終わるごとに資金は減らないので、次のサイクルは前回と同じかそれ以上の資金で始まります。
したがって、最初の \(1\) サイクルで倒産しなければ、その後も倒産しません。
よって、区間 \([L,L+N-1]\) で
\[ p_k < p_{L-1}-S \]
となる最初の位置 \(k\) を探します。
見つかれば答えは
\[ k-L+1 \]
日目です。
見つからなければ答えは \(0\) です。
\(T < 0\) の場合
\(1\) サイクルごとに資金が減っていくので、いつか必ず倒産します。
ただし、初期資金 \(S\) が大きい場合、何サイクルも耐える可能性があります。
これを毎日シミュレーションすると間に合いません。
まず、\(L\) 日目から始めた \(1\) サイクル内での資金増減の最小値を求めます。
区間 \([L,L+N-1]\) における累積和の最小値を \(\min p_k\) とすると、\(1\) サイクル内での最悪の増減は
\[ m = \min_{k \in [L,L+N-1]} p_k - p_{L-1} \]
です。
\(c\) サイクル経過後にそのサイクルを始めると、開始時の資金は
\[ S + cT \]
です。
そのサイクル中の最小資金は
\[ S + cT + m \]
になります。
これが初めて負になるような最小の \(c\) を求めます。
\(T<0\) なので、\(D=-T\) とすると、資金はサイクルごとに \(D\) 減ります。
もし
\[ S+m<0 \]
なら、最初のサイクルで倒産するので \(c=0\) です。
そうでなければ、
\[ c = \left\lfloor \frac{S+m}{D} \right\rfloor + 1 \]
です。
この \(c\) は「倒産が起こるサイクルの前に、何サイクル完了しているか」を表します。
そのサイクル内では、倒産条件は
\[ S+cT+p_k-p_{L-1}<0 \]
です。
変形すると、
\[ p_k < p_{L-1}-S-cT \]
となります。
再びセグメント木で、区間 \([L,L+N-1]\) の中からこの条件を初めて満たす位置 \(k\) を探します。
答えは
\[ cN + (k-L+1) \]
です。
アルゴリズム
- 各日の増減 \(d_i=A_i-B_i-C_i\) を計算する。
- 周期をまたぐ区間を扱うため、\(2N\) 日分の累積和 \(p\) を作る。
- 累積和 \(p\) に対してセグメント木を構築する。
- 各質問 \((L,S)\) について処理する。
- \(base=p_{L-1}\)
- \(R=L+N-1\)
- \(T=p_N\)
- \(T \geq 0\) の場合
- \(p_k < base-S\) となる最初の \(k \in [L,R]\) を探す。
- 見つかれば \(k-L+1\)、なければ \(0\) を出力する。
- \(T < 0\) の場合
- 区間 \([L,R]\) の最小値を求め、\(m=\min p_k-base\) とする。
- 倒産が起こるサイクル番号 \(c\) を求める。
- \(p_k < base-S-cT\) となる最初の \(k \in [L,R]\) を探す。
- \(cN+(k-L+1)\) を出力する。
計算量
- 時間計算量: \(O((N+Q)\log N)\)
- 空間計算量: \(O(N)\)
実装のポイント
倒産条件は「資金が \(0\) 円未満」なので、判定は
\[ < 0 \]
です。
そのため、累積和を探すときも
\[ p_k < \text{limit} \]
という strict な不等号になります。
\( \leq \) にしてしまうと、資金がちょうど \(0\) 円の場合まで倒産扱いしてしまうので注意が必要です。
また、累積和や答えは大きくなる可能性があるため、long long を使います。
ソースコード
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (1LL << 62);
struct SegTree {
int n, m;
vector<ll> seg;
SegTree(const vector<ll>& a) {
m = (int)a.size();
n = 1;
while (n < m) n <<= 1;
seg.assign(2 * n, INF);
for (int i = 0; i < m; i++) seg[n + i] = a[i];
for (int i = n - 1; i >= 1; i--) {
seg[i] = min(seg[i << 1], seg[i << 1 | 1]);
}
}
ll range_min(int l, int r) const {
ll res = INF;
l += n;
r += n;
while (l <= r) {
if (l & 1) res = min(res, seg[l++]);
if (!(r & 1)) res = min(res, seg[r--]);
l >>= 1;
r >>= 1;
}
return res;
}
int first_less_rec(int node, int nl, int nr, int ql, int qr, ll x) const {
if (qr < nl || nr < ql || seg[node] >= x) return -1;
if (nl == nr) return nl;
int mid = (nl + nr) >> 1;
int res = first_less_rec(node << 1, nl, mid, ql, qr, x);
if (res != -1) return res;
return first_less_rec(node << 1 | 1, mid + 1, nr, ql, qr, x);
}
int first_less(int l, int r, ll x) const {
return first_less_rec(1, 0, n - 1, l, r, x);
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, Q;
cin >> N >> Q;
vector<ll> d(N + 1);
for (int i = 1; i <= N; i++) {
ll A, B, C;
cin >> A >> B >> C;
d[i] = A - B - C;
}
vector<ll> pref(2 * N + 1, 0);
for (int i = 1; i <= 2 * N; i++) {
pref[i] = pref[i - 1] + d[(i - 1) % N + 1];
}
ll total = pref[N];
SegTree seg(pref);
while (Q--) {
int L;
ll S;
cin >> L >> S;
ll base = pref[L - 1];
int R = L + N - 1;
if (total >= 0) {
ll limit = base - S;
int idx = seg.first_less(L, R, limit);
if (idx == -1) {
cout << 0 << '\n';
} else {
cout << (ll)(idx - L + 1) << '\n';
}
} else {
ll min_pref = seg.range_min(L, R);
ll mn = min_pref - base;
ll cycles = 0;
ll val = S + mn;
if (val >= 0) {
ll dec = -total;
cycles = val / dec + 1;
}
ll threshold = -S - cycles * total;
ll limit = base + threshold;
int idx = seg.first_less(L, R, limit);
ll ans = cycles * (ll)N + (ll)(idx - L + 1);
cout << ans << '\n';
}
}
return 0;
}
この解説は gpt-5.5-xhigh によって生成されました。
投稿日時:
最終更新: