Appearance
Planar Reflections -- CF 1498C
Difficulty: 1600 * Techniques: DP, state design (planes x energy), symmetry collapse Problem Link
Quick Revisit
- PROBLEM: Count particles exiting after reflections between n planes, starting with energy k
- KEY INSIGHT: Don't simulate particles. Define dp[i][j] = exits from one particle with i planes ahead, energy j. Direction collapses by symmetry.
- RECURRENCE: dp[i][j] = dp[i-1][j] + dp[i-1][j-1], with dp[0][j] = dp[i][0] = 1. Answer = dp[n][k].
- TRANSFER: Exponential blowup of objects => DP by state, not individual tracking. Use symmetry to drop redundant state dimensions.
First Read
Constraints:
The Simulation Disaster
My first reaction: simulate every particle. Each plane hit doubles the live count, so after
The right move on exponential blowup is count by state, don't track individuals. So I need a DP whose state is small.
First Attempt at State Design (and Why It Stalled)
I tried "
The fix is to collapse direction by symmetry. Once you stop following individual trajectories and instead ask "given a single particle that has
The State That Works
Let
When that particle hits the next plane:
- It passes through with
planes still ahead and energy , contributing . - If
, it spawns a copy with energy going the other way. The copy now plays the same game in the other direction with one less energy, contributing by the symmetry above.
Base cases:
: no planes ahead means the particle just exits. : energy 0 never spawns, so it just walks through the remaining planes and exits.
Recurrence:
The answer is
Verifying the recurrence on tiny cases
: simulate by hand to get 3 exits. The DP gives . Match. : simulate to get 4 exits. The DP gives . Match. : simulate to get 7 exits. The DP gives . Match.
The "the symmetry collapses direction" claim is exactly what makes the spawn term
The Code
cpp
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
void solve() {
int n, k;
cin >> n >> k;
// dp[j] rolled over i; dp[0][j] = 1 for all j
vector<long long> dp(k + 1, 1);
for (int i = 1; i <= n; i++) {
vector<long long> ndp(k + 1);
ndp[0] = 1;
for (int j = 1; j <= k; j++)
ndp[j] = (dp[j] + dp[j - 1]) % MOD;
dp = ndp;
}
cout << dp[k] << "\n";
}
int main() {
int t; cin >> t;
while (t--) solve();
}Transfer Lesson
- Exponential blowup of objects => DP by state, not by particle -- when a process multiplies objects, you almost never simulate each one. Find the smallest state that captures their future behavior.
- Collapse direction by symmetry -- the breakthrough was realizing left-vs-right doesn't matter once you parameterize by "planes ahead." Whenever a process has a mirror symmetry, look for a state that is invariant under it.
- Pascal-shaped recurrence, but not literal Pascal -- the recurrence
has the shape of Pascal's triangle, but the boundary is different from . Don't be fooled by shape into trusting a closed form without verifying boundaries. - A wrong DP is just a state that missed a feature -- here, the first attempt missed direction. When a DP feels like it's branching unpredictably, ask which physical attribute you forgot to encode.
See also: DP Intro | DP Thinking Framework | Pattern Recognition Guide | Practice Ladder 1400-1700