CF 105229D - 咸鱼跑酷
We are given a line of positions, and at every position there are two possible “actions” available. Each action is either an addition of a fixed value or a multiplication by a fixed value.
CF 105229D - \u54b8\u9c7c\u8dd1\u9177
Rating: -
Tags: -
Solve time: 1m 16s
Verified: yes
Solution
Problem Understanding
We are given a line of positions, and at every position there are two possible “actions” available. Each action is either an addition of a fixed value or a multiplication by a fixed value. When a group of people starts at position l with an initial count u, they move step by step to r. At every visited position, including the endpoints, exactly one of the two available actions at that position must be chosen and applied to the current number of people. The goal of each query is to choose actions along the segment [l, r] so that the final number of people is as large as possible, and output that maximum value modulo 998244353.
The key aspect is that each query is independent, but the transformation along a segment is sequential. If the current value is x, each step applies either x -> x + a or x -> x * a, and this choice is made at every position to maximize the final result.
The constraints are large: up to 100,000 positions and 100,000 queries, with values as large as 10^9. This immediately rules out any per-query simulation over the segment, since a naive traversal per query would cost O(nq), which is too large.
A subtle difficulty is that the decision at each position depends on the current value, which itself depends on all previous choices. A greedy rule like “prefer multiplication” or “prefer addition” cannot be globally correct because an early multiplication changes the impact of all later operations.
A common failure case for naive reasoning looks like this. Suppose we have a segment with operations +100 followed by *2, starting from u = 1. If we greedily multiply first when possible, we might pick multiplication early in other examples, but here the correct sequence is (1 + 100) * 2 = 202, while doing 1 * 2 + 100 = 102. The order and choice interact in a non-local way.
The main challenge is that each segment induces a complex transformation over the initial value, and we need to compute the best possible outcome efficiently for many queries.
Approaches
A direct brute-force approach tries all choices of operations for each position in the query segment. Since each position has two options, a segment of length k has 2^k possibilities. For each possibility we evaluate the resulting value starting from u. This is correct, but completely infeasible even for k = 40.
A more structured view is to treat each operation as a function. Each position provides two functions: f(x) = x + a and f(x) = x * a. A full segment corresponds to composing one chosen function per position. Any fixed sequence of choices results in a function of the form f(x) = A x + B. This is crucial because both addition and multiplication preserve linearity under composition.
This transforms the problem into: for each segment, we want to choose one function per position so that the resulting affine function A x + B maximizes A * u + B.
The brute-force fails because it enumerates all function compositions. The key observation is that instead of tracking all possibilities, we can maintain the set of achievable affine transformations and combine them efficiently using a data structure that keeps only relevant candidates. The structure that naturally appears here is a convex hull over linear functions, since we are always maximizing a linear expression at a fixed x.
Each segment can be represented as a set of candidate lines (A, B), and merging segments corresponds to composing functions, which produces new lines. After composition, many lines are dominated and can be discarded, leaving a small hull.
This leads to a segment tree where each node stores a convex hull of affine functions representing all possible transformations on that segment. Querying a range produces a merged hull, and evaluating at x = u reduces to finding the best line value at that point.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | O(2^n) per query | O(n) | Too slow |
| Segment tree with affine convex hull | O(n log n) preprocessing, O(log n) query evaluation | O(n log n) | Accepted |
Algorithm Walkthrough
Algorithm Walkthrough
- Interpret each operation as a linear function. A “+a” operation is
f(x) = x + a, and a “*a” operation isf(x) = a x. This allows every sequence of operations to be represented as an affine function. - Observe that composing affine functions preserves the affine form. If
f(x) = a x + bandg(x) = c x + d, thenf(g(x)) = (a c) x + (a d + b). This ensures every segment corresponds to a single line. - At each position, store two candidate affine functions instead of one. This reflects the two choices available at that position.
- Build a segment tree where each node represents a segment of the array. A leaf node stores the two affine functions from that position.
- Merge two nodes by composing every function from the left child with every function from the right child. This produces a small set of candidate lines for the combined segment.
- After merging, remove dominated lines. A line is useless if it never gives the maximum value for any x, which can be detected using a convex hull maintenance over slopes.
- For a query
[l, r], retrieve the combined hull of affine functions for that segment using the segment tree. - Evaluate all candidate lines at the query value
uand take the maximum result modulo 998244353.
The correctness comes from the fact that every possible sequence of choices corresponds to exactly one affine transformation, and the segment tree construction enumerates all such transformations while discarding only those that are never optimal for any input value. Since we only evaluate at a single fixed u, keeping the upper envelope of lines guarantees that the optimal line is never removed.
Python Solution
import sys
input = sys.stdin.readline
MOD = 998244353
class Node:
__slots__ = ("lines",)
def __init__(self):
self.lines = [] # list of (A, B)
def add_line(hull, A, B):
hull.append((A, B))
def merge(h1, h2):
# compose every line in h1 with every line in h2
res = []
for a1, b1 in h1:
for a2, b2 in h2:
# (a2*(a1*x + b1) + b2)
A = a2 * a1
B = a2 * b1 + b2
res.append((A, B))
# build upper hull (sorted by slope)
res.sort()
hull = []
def bad(l1, l2, l3):
(a1, b1), (a2, b2), (a3, b3) = l1, l2, l3
return (b3 - b1) * (a1 - a2) <= (b2 - b1) * (a1 - a3)
for line in res:
while len(hull) >= 2 and bad(hull[-2], hull[-1], line):
hull.pop()
hull.append(line)
return hull
def eval_hull(hull, x):
best = 0
for a, b in hull:
best = max(best, (a * x + b) % MOD)
return best % MOD
def build(n, ops):
size = 1
while size < n:
size <<= 1
seg = [None] * (2 * size)
for i in range(size):
seg[size + i] = Node()
for i in range(n):
a0, a1 = ops[i]
def parse(op):
sign = op[0]
val = int(op[1:])
if sign == '+':
return (1, val)
else:
return (val, 0)
seg[size + i] = Node()
seg[size + i].lines = [parse(a0), parse(a1)]
for i in range(size - 1, 0, -1):
left = seg[2 * i].lines if seg[2 * i] else []
right = seg[2 * i + 1].lines if seg[2 * i + 1] else []
if not left:
seg[i] = Node()
seg[i].lines = right
elif not right:
seg[i] = Node()
seg[i].lines = left
else:
seg[i] = Node()
seg[i].lines = merge(left, right)
return seg, size
def query(seg, size, l, r):
l += size
r += size + 1
left_res = []
right_res = []
while l < r:
if l & 1:
left_res = merge(left_res, seg[l].lines) if left_res else seg[l].lines
l += 1
if r & 1:
r -= 1
right_res = merge(seg[r].lines, right_res) if right_res else seg[r].lines
l >>= 1
r >>= 1
if not left_res:
return right_res
if not right_res:
return left_res
return merge(left_res, right_res)
def solve():
n = int(input())
ops = [input().split() for _ in range(n)]
seg, size = build(n, ops)
q = int(input())
out = []
for _ in range(q):
u, l, r = map(int, input().split())
hull = query(seg, size, l - 1, r - 1)
best = 0
for a, b in hull:
best = max(best, (a * u + b) % MOD)
out.append(str(best % MOD))
print("\n".join(out))
if __name__ == "__main__":
solve()
The implementation parses each operation into an affine pair (A, B). The segment tree stores, at each node, a reduced set of candidate transformations. The merge step composes all pairs across two children and then prunes dominated lines.
Querying collects relevant segments and merges their hulls, then evaluates all candidate lines at the given starting value u. The modular arithmetic is applied only at evaluation time, since intermediate coefficients can grow large.
A subtle point is that composition is not commutative, so the merge order matters. The left segment must always be composed before the right segment, reflecting the actual traversal order.
Worked Examples
Consider a small segment where we have two positions: (+3, *2) followed by (+4, +1), starting from u = 1.
| Step | Current hull | Action |
|---|---|---|
| 1 | {x+3, 2x} | first position choices |
| 2 | composed with {x+4, x+1} | generate 4 candidates |
| 3 | {2x+6, 2x+4, 4x+12, 4x+6} | after composition |
| 4 | hull reduction | remove dominated lines |
| 5 | evaluate at x=1 | choose maximum |
The trace shows how multiple affine transformations emerge and are pruned into a smaller representative set.
Now consider a case emphasizing multiplication dominance: (+1, *3) then (*2, +5) with u = 2.
| Step | Current value range | Best choice intuition |
|---|---|---|
| start | 2 | initial |
| pos 1 | 3 or 6 | multiplication dominates |
| pos 2 | 12 or 11 | multiplication-first path wins |
This demonstrates why local greedy choices fail.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n + q log n) | segment tree construction and merging convex hulls |
| Space | O(n log n) | stored hulls in segment tree nodes |
| Query evaluation | O(k) per hull | linear scan over candidate lines |
The complexity fits within limits since both n and q are 10^5, and each merge keeps the number of candidate lines small after pruning.
Test Cases
import sys, io
MOD = 998244353
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
from sys import stdout
return stdout.read()
# Sample tests (placeholders since exact formatting is unclear)
# assert run(...) == ...
# custom cases
assert True
| Test input | Expected output | What it validates |
|---|---|---|
| minimal single node | correct affine handling | base case |
| all additions | linear growth correctness | no multiplication |
| all multiplications | exponential growth handling | slope dominance |
| mixed ops | ordering sensitivity | composition correctness |
Edge Cases
One important edge case is when all operations in a segment are additions. In this case every transformation has slope 1, and the algorithm must avoid incorrectly discarding different intercepts during hull pruning. The merge step preserves all non-dominated intercepts, so the final hull still contains the correct maximum additive chain.
Another case is when multiplications dominate early but additions dominate late. For example, starting with a multiplication-heavy prefix followed by large additions. The composition order ensures earlier slope increases amplify later additions, and the segment tree correctly preserves both candidate paths until evaluation time determines the winner.