問題
キュー(先入れ先出し)に 一次関数 $f(x) = a x + b$ を出し入れする。次の $Q$ 個の操作を順に処理せよ。すべての計算は $\bmod\ 998244353$ で行う。
0 a b: 関数 $f(x)=ax+b$ をキューの 末尾 に追加する1: キューの 先頭 の関数を取り除く(キューは空でないことが保証される)2 x: キューの中身を先頭から順に $f_1, f_2, \dots, f_k$ とするとき、$f_k(\cdots f_2(f_1(x))\cdots) \bmod 998244353$ を出力する(キューが空なら $x$ をそのまま出力)
入力形式
Q
query_1
...
query_Q
制約
| # | 条件 |
|---|---|
| 1 | $1 \le Q \le 5 \times 10^5$ |
| 2 | $1 \le a < 998244353$, $0 \le b, x < 998244353$ |
| 3 | 1 の操作時、キューは空でない |
入出力例
入力例1
7
0 1 2
0 3 4
2 1
1
2 1
1
2 1出力例1
13
7
1($f_1=x+2, f_2=3x+4$。$f_2(f_1(1))=3\cdot3+4=13$。$f_1$ を捨てると $f_2(1)=7$。空になると $1$)
この問題は Library Checker の「Queue Operate All Composite」(https://judge.yosupo.jp/problem/queue_operate_all_composite 、2026-09-23 参照)と同じ形式で、そこでの制約は $Q \le 5\times10^5$。
概念図:2本のスタックと集約の向き
ヒント(クリックで段階的に開示)
ヒント1(方向性)
関数合成には 逆元が無い($a$ が 0 でないので本問では実は逆関数が作れるが、一般のモノイド——min、max、gcd、行列積の非正則なもの——では作れない)。「全体から先頭を引き算する」累積和型の発想は使えない。引き算を使わずに、先頭を消せる集約を考える。
ヒント2(アプローチ)
キューは「スタック2本」で作れる(入れる用 back と出す用 front)。スタックは末尾でしか変化しないので、各要素に "自分から底までの集約値" を持たせておけば、push/pop で集約値を壊さずに済む。キュー全体の集約 = front の top が持つ集約 と back 全体の集約 の合成。
ヒント3(誘導)
def comp(f, g): # 「f を適用してから g」を 1 本の一次関数に
return (f[0]*g[0] % MOD, (f[1]*g[0] + g[1]) % MOD)
# push: back に積み、back_fold = comp(back_fold, f)
# pop : front が空なら back を全部 pop しながら front に移す。
# 移すとき「その要素から (元の) 先頭側までの合成」ではなく
# 「その要素から キュー末尾側まで」… 向きに注意して acc = comp(f, acc)
# fold: comp(front[-1] の集約, back_fold)模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
MOD = 998244353
ID = (1, 0) # 恒等関数 x -> x
def comp(f, g):
"""f を先に適用し、その後 g を適用した合成 g(f(x)) を返す。"""
return (f[0] * g[0] % MOD, (f[1] * g[0] + g[1]) % MOD)
def main():
data = sys.stdin.buffer.read().split()
q = int(data[0])
pos = 1
front = [] # 各要素 = 「その要素 ~ front の底(キュー中央側)」までの合成
back = [] # 生の関数
back_fold = ID # back 全体の合成(先に入った順に適用)
out = []
for _ in range(q):
t = data[pos]
if t == b'0':
f = (int(data[pos + 1]), int(data[pos + 2]))
pos += 3
back.append(f)
back_fold = comp(back_fold, f)
elif t == b'1':
pos += 1
if not front:
acc = ID
while back: # 末尾側から取り出す
f = back.pop()
acc = comp(f, acc) # f を先に、既に積んだ(より後ろの)関数を後に
front.append(acc)
back_fold = ID
front.pop()
else:
x = int(data[pos + 1])
pos += 2
a, b = comp(front[-1] if front else ID, back_fold)
out.append((a * x + b) % MOD)
sys.stdout.write("\n".join(map(str, out)) + ("\n" if out else ""))
main()
計算量: 各関数は back → front へ 高々1回 しか移動しないので、全操作で償却 $O(1)$、全体 $O(Q)$。空間 $O(Q)$。
Step-by-Step 解説
スライド窓の和なら「全体 − 先頭」で済むが、それは加法に逆元があるからである。min・max・gcd・行列積・関数合成(一般の場合)には逆元が無いので、先頭を「取り消す」手段がない。SWAG はこの「逆元が無いモノイドでも、キュー全体の集約を償却 O(1) で維持する」ための道具。
スタックは末尾だけが変わる。各要素に「底から自分までの集約」を持たせておけば、pop は単に1段下の値に戻るだけで、取り消し計算が要らない。キュー = 2本のスタック に分解すれば、この性質をキューにも持ち込める。
関数合成は 非可換 なので向きが命。キューの先頭から $f_1, f_2, \dots$ の順に適用する。
- back は「後から来たもの」が上に積まれる →
back_fold = comp(back_fold, f)(既存を先に、新しい f を後に) - front に移すときは back の 上(新しい方)から 取り出す。front の上にくる要素ほどキューの先頭側なので、front の各要素は「自分を先に、自分より後ろの関数群を後に」適用した合成を持つ →
acc = comp(f, acc) - 全体 =
comp(front[-1], back_fold)(front 側 = 先頭側を先に)
入力例1で確認すると、2 1 の時点で front は空、back_fold $= f_2\circ f_1 = (3, 3\cdot2+4)=(3,10)$ で $3\cdot1+10=13$。
移し替え(while back)は1回で $|back|$ 要素を動かすが、各要素は back に入ってから front を経て捨てられるまでに 1回しか移動しない。したがって全体で移動は $Q$ 回以下、各操作は償却 $O(1)$。ただし 最悪1回の pop は $O(Q)$ かかる(リアルタイム性が要る場面では問題になる → 次の「迷った他案」参照)。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| deque を毎回先頭から全部合成(素朴) | 1クエリ $O(k)$ | 比較用のみ | $Q=2\times10^4$ でも 1.168 秒(実測)。$Q=5\times10^5$ では TLE |
| セグメント木(時刻を添字に、pop は単位元で上書き) | 1操作 $O(\log Q)$ | 不採用 | 正しく動き、区間クエリにも拡張できる。ただし本問は「キュー全体」しか問わないので $\log$ と配列 $2Q$ 個が余計。区間の任意部分を問う形に変わったらこちらに切り替える |
| 逆関数を使う($a \ne 0$ なので $f^{-1}(x) = a^{-1}(x-b)$) | 1操作 $O(\log MOD)$(逆元計算) | 不採用 | 本問は偶然 $a\ge1$ なので可能だが、$a=0$ を許す・min/max/gcd に変わる、と即座に破綻する。一般性を失う割に速くない |
| SWAG(本解) | 償却 $O(1)$ | 採用 | 逆元不要・実装 30 行。最悪ケースの1操作 $O(Q)$ は競プロでは問題にならない |
| Two-Stack の de-amortized 版(Okasaki 型の段階的移し替え)/ DABA | 最悪 $O(1)$ | 扱わない | ストリーム処理(Apache Flink 等の窓集約)でレイテンシのばらつきを嫌う場合の選択肢。競プロでは償却で十分なので本問の範囲外とした |
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
合成の向きを逆にする(comp(acc, f)) | 関数合成を可換だと思い込む | front へ移すときは「取り出した f を先に、既に積んだ後方の合成を後に」 acc = comp(f, acc) |
移し替えのあと back_fold を単位元に戻し忘れる | back が空になったことを集約値に反映しない | back_fold = ID を移し替えの最後で必ず行う |
空キューで front[-1] を参照 | 空の場合の単位元を用意していない | front[-1] if front else ID |
| 入出力が遅い | input() を $5\times10^5$ 回呼ぶ | sys.stdin.buffer.read().split() で一括読み込み |
% MOD を付け忘れて多倍長整数が膨張 | Python は溢れないので気づきにくい | 合成のたびに % MOD(付け忘れると値が桁数を増やし続け、遅くなるだけで WA にならないので発見が遅れる) |
次のステップ
- 発展1: 「キュー」ではなく 両端キュー(deque) に集約を持たせる(Library Checker「Deque Operate All Composite」)。2スタックを「空になった側に反対側の半分を移す」形に変えると償却 O(1) を保てる
- 発展2: 窓の長さ $K$ 固定のスライド最小値を SWAG で書き、単調デック(Day074・Day126 のモノトニックキュー)と実測比較する——単調デックは「比較可能な値の min/max」専用、SWAG は任意のモノイドで使える、という守備範囲の違いを体感する
- 扱わなかった範囲: 最悪 $O(1)$ 保証版(DABA: De-Amortized Banker's Aggregator、Tangwongsan ら 2017)と、その実務での用途(ストリーム処理の窓集約)
検証ログ(本セッションで実行した数値)
- 入力例1: SWAG 版
[13, 7, 1]、素朴版[13, 7, 1]で一致 - ランダムテスト: 小さい値($Q\le40$、係数 $\le10$)1000 件 + 係数を $\bmod$ 全域にとった $Q=300$ を 200 件、計 1200 件で素朴版と比較 → 不一致 0 件
- 模範解答そのもの(標準入力版)でも、ランダム 100 件($Q\le60$)を素朴版と比較 → 不一致 0 件
- 性能(実測): $Q=2\times10^4$(push 55%・pop 20%・fold 25%)で SWAG 0.006 秒 / 素朴版 1.168 秒(出力一致)
- 性能(実測): $Q=2\times10^5$、キューの最大長 69,877 で SWAG 0.057 秒(素朴版は1回の fold で最大 7 万回の合成になるため計測を省略)
- 検証スクリプト: スクラッチパッドの
q1.py(上記は実行結果の転記)