For a permutation , call a subarray an interval if its values form a contiguous range, i.e. . The permutation tree (also called the cut/join tree) encodes all intervals in nodes. Problems: count intervals (CF 526F), count intervals inside a query range (CF 997E), shortest interval containing given positions (gym 101620 I), count permutations with no nontrivial interval (simple permutations).
Counting intervals without the tree
For a fixed right end (exclusive), let . Then , with equality iff is an interval. Keep in a segment tree with range add and “min + count of min”:
- moving forward subtracts 1 from every , (the length term) and sets ;
- the max term changes on a contiguous block of per popped element of a monotonic max-stack; do a range add of the difference. Same for the min-stack.
The number of intervals ending at is the number of zeros among , which is the count of the minimum when the minimum is 0. Total .
// seg: range add, query (min, count of min) over [0, n)
ll countIntervals(const vector<int>& p) {
int n = sz(p); ll ans = 0; vector<int> mx, mn; // stacks of indices
for (int r = 0; r < n; r++) {
while (sz(mx) && p[mx.back()] < p[r]) { int j = mx.back(); mx.pop_back(); seg.add(sz(mx) ? mx.back() + 1 : 0, j + 1, p[r] - p[j]); }
mx.push_back(r);
while (sz(mn) && p[mn.back()] > p[r]) { int j = mn.back(); mn.pop_back(); seg.add(sz(mn) ? mn.back() + 1 : 0, j + 1, p[j] - p[r]); }
mn.push_back(r);
if (r) seg.add(0, r, -1); // the length term grew by one
auto [m, c] = seg.query(0, r + 1);
ans += m == 0 ? c : 0;
}
return ans;
}For “number of intervals inside ” (CF 997E), add a lazy tag “add the current zero-count to a per-node historic counter” and apply it to the whole prefix after every step; then answer queries with right end by a range query of historic counts over .
Proof: with equality exactly on intervals
has distinct integers between its min and max, so , which is . Equality means the distinct values fill the whole range , which is the definition of an interval. When increases, the max over changes only for the between the new element and the previous greater element, and on each popped stack block the old max was the popped value, so adding on that block updates the term exactly; similarly for the min.
Structure
Two intervals overlap if they intersect and neither contains the other.
Lemma: if intervals and overlap, then , , and are intervals
As position ranges, and are contiguous. Their value sets: and are integer ranges that intersect (they share the values at the positions of ), so is an integer range and is an interval. , an intersection of ranges, so is an interval. is contiguous in positions (since neither contains the other) and its values are , a range minus a range that is not strictly inside it (otherwise would have size ), hence a range.
An interval is strong if no interval overlaps it. Strong intervals form a laminar family (any two are nested or disjoint), so they form a tree: the root is , the leaves are single positions, and the children of a node are its maximal strong proper subintervals, which partition it. Read the children’s value blocks in order; their relative order (the quotient permutation) is either
- monotone (a join node, increasing or decreasing): every union of consecutive children is an interval; or
- simple (a cut/prime node): no union of consecutive children is an interval. A simple permutation has (none exist for ).
Every interval is either a node or a union of consecutive children of a join node. So the number of intervals is
Proof: the classification
Let be an interval and the smallest strong interval containing it. Every child of is strong, so it doesn’t overlap : either , , or , and the last contradicts the minimality of . So is a union of consecutive children.
Now the quotient of (sketch; this is the substitution decomposition theorem of Albert and Atkinson). If some union of consecutive children is an interval, it is not strong (it would lie strictly between and its children, contradicting their maximality), so some interval overlaps it; because is strong, so is also a union of children. Take maximal: by the lemma is a strictly larger interval, so . Thus splits into consecutive blocks , , , all intervals, with and intervals, so their values are monotone (say ). Repeating the argument on the maximal proper prefix interval and suffix interval shows that every boundary between consecutive children separates smaller values from larger ones in the same direction, so the quotient is the identity or its reverse. Hence either the quotient has no nontrivial interval (cut node: then or is a single child, both nodes), or it is monotone (join node: every consecutive union is an interval).
Counting: each node is one interval; a join node with children additionally contributes the consecutive unions of at least 2 but not all children.
construction
Sweep with the -segment tree above and a stack of nodes whose position ranges tile . When a new leaf arrives, repeatedly:
- if the top of the stack is a join node whose last child is adjacent in value to the current node in the same direction, append the current node as its child;
- else if the top and the current node are adjacent in value, create a new join node with these two children;
- else if the segment tree says some to the left gives an interval ending here (min of over is 0), pop nodes until their union is an interval and make them children of a new cut node;
- otherwise stop and push the current node.
struct PermutationTree { // p: permutation of 0..n-1
int n, root = -1, cnt = 0, LOG = 1;
vector<vector<int>> ch; // children ordered by position; leaves are nodes 0..n-1
vector<int> type, lo, hi, vlo, vhi, par; // type 0 leaf/cut, 1 increasing join, 2 decreasing join; [lo, hi), [vlo, vhi)
vector<vector<int>> up; vector<int> mn, lz;
void app(int p, int v) { mn[p] += v, lz[p] += v; }
void push(int p) { if (lz[p]) app(2 * p, lz[p]), app(2 * p + 1, lz[p]), lz[p] = 0; }
void update(int p, int b, int e, int l, int r, int v) { // closed ranges, 1-indexed
if (r < b || e < l || l > r) return;
if (l <= b && e <= r) return app(p, v);
push(p); int m = (b + e) / 2;
update(2 * p, b, m, l, r, v); update(2 * p + 1, m + 1, e, l, r, v);
mn[p] = min(mn[2 * p], mn[2 * p + 1]);
}
int query(int p, int b, int e, int l, int r) {
if (r < b || e < l || l > r) return INT_MAX;
if (l <= b && e <= r) return mn[p];
push(p); int m = (b + e) / 2;
return min(query(2 * p, b, m, l, r), query(2 * p + 1, m + 1, e, l, r));
}
PermutationTree(const vector<int>& perm) : n(sz(perm)) {
if (!n) return;
int N = 2 * n + 2; vector<int> p(n + 1);
for (int i = 0; i < n; i++) p[i + 1] = perm[i] + 1;
vector<vector<int>> g(N); vector<int> ty(N), pa(N, -1), rl(N), rr(N), sl(N), sr(N);
mn.assign(4 * n + 4, 0), lz.assign(4 * n + 4, 0);
auto addEdge = [&](int u, int v) { pa[v] = u, g[u].push_back(v); };
auto adj = [&](int i, int j) { return rr[i] == rl[j] - 1; };
auto len = [&](int i) { return rr[i] - rl[i] + 1; };
int id = n + 1; vector<int> mx = {0}, mi = {0}, st;
for (int i = 1; i <= n; i++) {
while (mx.back() && p[mx.back()] < p[i]) { int r = mx.back(); mx.pop_back(); update(1, 1, n, mx.back() + 1, r, p[i] - p[r]); }
mx.push_back(i);
while (mi.back() && p[mi.back()] > p[i]) { int r = mi.back(); mi.pop_back(); update(1, 1, n, mi.back() + 1, r, p[r] - p[i]); }
mi.push_back(i);
rl[i] = rr[i] = p[i]; sl[i] = sr[i] = i;
int cur = i;
while (true) {
if (sz(st) && (adj(st.back(), cur) || adj(cur, st.back()))) {
int b = st.back();
if ((adj(b, cur) && ty[b] == 1) || (adj(cur, b) && ty[b] == 2)) {
addEdge(b, cur);
rl[b] = min(rl[b], rl[cur]), rr[b] = max(rr[b], rr[cur]), sl[b] = min(sl[b], sl[cur]), sr[b] = max(sr[b], sr[cur]);
cur = b; st.pop_back();
} else {
ty[id] = adj(b, cur) ? 1 : 2; addEdge(id, b); addEdge(id, cur);
rl[id] = min(rl[b], rl[cur]), rr[id] = max(rr[b], rr[cur]), sl[id] = min(sl[b], sl[cur]), sr[id] = max(sr[b], sr[cur]);
st.pop_back(); cur = id++;
}
} else if (query(1, 1, n, 1, i - len(cur)) == 0) {
int L = len(cur), a = rl[cur], b = rr[cur], s1 = sl[cur], s2 = sr[cur];
addEdge(id, cur);
do {
int t = st.back(); L += len(t);
a = min(a, rl[t]), b = max(b, rr[t]), s1 = min(s1, sl[t]), s2 = max(s2, sr[t]);
addEdge(id, t); st.pop_back();
} while (b - a + 1 != L);
reverse(all(g[id]));
rl[id] = a, rr[id] = b, sl[id] = s1, sr[id] = s2; cur = id++;
} else break;
}
st.push_back(cur);
update(1, 1, n, 1, i, -1);
}
cnt = id - 1;
ch.assign(cnt, {}), type.assign(cnt, 0), lo.assign(cnt, 0), hi.assign(cnt, 0), vlo.assign(cnt, 0), vhi.assign(cnt, 0), par.assign(cnt, -1);
for (int u = 1; u <= cnt; u++) {
int x = u - 1;
for (int v : g[u]) ch[x].push_back(v - 1);
type[x] = ty[u]; lo[x] = sl[u] - 1, hi[x] = sr[u]; vlo[x] = rl[u] - 1, vhi[x] = rr[u];
par[x] = pa[u] == -1 ? -1 : pa[u] - 1;
if (pa[u] == -1) root = x;
}
while ((1 << LOG) < cnt) LOG++;
up.assign(LOG, vector<int>(cnt));
for (int u = 0; u < cnt; u++) up[0][u] = par[u] == -1 ? u : par[u];
for (int k = 1; k < LOG; k++) for (int u = 0; u < cnt; u++) up[k][u] = up[k - 1][up[k - 1][u]];
}
pair<int, int> minimalInterval(int l, int r) const { // shortest interval containing positions [l, r)
if (r - l == 1) return {l, r};
int u = l;
for (int k = LOG - 1; k >= 0; k--) if (hi[up[k][u]] < r) u = up[k][u];
u = par[u];
if (type[u] == 0) return {lo[u], hi[u]};
const auto& c = ch[u];
int a = *partition_point(all(c), [&](int v) { return hi[v] <= l; });
int b = *(partition_point(all(c), [&](int v) { return lo[v] < r; }) - 1);
return {lo[a], hi[b]};
}
ll countIntervals() const {
ll t = 0;
for (int u = 0; u < cnt; u++) { t++; if (type[u]) { ll k = sz(ch[u]); t += k * (k - 1) / 2 - 1; } }
return t;
}
};Proof: the construction is and builds the tree
Each step of the inner loop either attaches a node to an existing join node, creates a new node, or stops. Nodes are created at most times and each node is popped from the stack at most once, so the loop runs times in total, each with segment tree work; the stacks for max/min add range updates. The stack invariant is that its nodes tile and no union of a suffix of them with the current node is an interval unless detected: the segment tree query over left endpoints before the current node’s start detects exactly that some suffix union ending at is an interval. A suffix union that is an interval either has two members adjacent in value (handled as join) or needs at least four members whose union first becomes an interval at a cut node, which is why the cut case pops until the value range has no gaps. The resulting nodes are exactly the strong intervals, matching the structure above.
Uses
- Minimal interval containing positions : lift from leaf to the deepest ancestor not containing , take its parent ; if is a cut node the answer is , otherwise the union of ‘s children from the one containing to the one containing . .
- Counting simple permutations / permutations by interval structure: decompose by the root type; the generating function of simple permutations satisfies a functional equation with the one of all permutations.
- “Sort by reversing intervals” and “split into maximum number of interval pieces”: DP over the tree; a join node’s children can be grouped freely.
- Interval-based DP on permutations (“number of subarrays that are intervals and satisfy X”): iterate over nodes, handling join nodes’ consecutive unions in or .