PERMREACH - Editorial

PROBLEM LINK:

Practice
Contest: Division 1
Contest: Division 2
Contest: Division 3
Contest: Division 4

Author: raysh07
Tester: tabr
Editorialist: iceknight1093

DIFFICULTY:

TBD

PREREQUISITES:

None

PROBLEM:

You’re given a permutation P of length N.
It’s called K-sortable if it’s possible to add K to some of its elements and obtain a sorted array.
Find the sum of all K such that P is K-sortable.

EXPLANATION:

First, we solve a simpler problem: given K, how do we decide whether P is K-sortable?
That problem can be solved by greedily increasing only the elements that need to be increased, that is:

  • For each i from 1 to N-1 in order, if P_i \gt P_{i+1}, increase P_{i+1} by K.
  • In the end, check if P is sorted.
Proof

Suppose P is K-sortable, say by operating on indices i_1, i_2, \ldots, i_m, where i_j \lt i_{j+1}.

We’ll perform these operations in order from left to right (since the order they’re performed in doesn’t affect the final result).

Looking at index i_1 before it was operated on:

  • If P_{i_1 - 1} \gt P_{i_1}, then our greedy algorithm would’ve increased index i_1.
  • Otherwise, increasing P_{i_1} serves no purpose: everything before i_1 is already sorted, everything after i_1 + 1 will become sorted by future moves, and not operating on P_{i_1} will still ensure that it doesn’t exceed P_{i_1 + 1} (no matter whether P_{i+1} is operated on or not).
    So, in such a case, we can simply discard the operation on i_1 and the sortedness won’t change.

Repeat this argument for i_2, then i_3, and so on.
The final result is that we end up with exactly the set of moves used by the greedy algorithm, so if it’s possible to sort P, the greedy will find it.


With this in mind, let’s attempt to find how small or large K can be.
First, note that if P_i \gt P_{i+1} for some index i, P_{i+1} must definitely be increased.
Since the final array must be sorted, certainly we must have K\geq P_i - P_{i-1}.

Let L be the maximum across all such differences (or 1 if no such index exists).
That is,

L = \max\left(\{P_i - P_{i-1} : 1 \leq i \lt N \text{ and } P_i \gt P_{i+1}\} \cup \{1\}\right)

L is then a lower bound for K, i.e, for any K\lt L, P is definitely not K-sortable.

Now, note that when P_i is increased, it might cause P_{i+1} to require an increase, even if P_i \lt P_{i+1} initially.
More generally, consider two indices x and y (x\lt y) such that:

  • P_x \gt P_{x+1}
  • P_{x+1} \lt P_{x+2} \lt \ldots \lt P_y
  • P_y \gt P_{y+1}

That is, the block from index x+1 to y is a maximal increasing block in P.
For such a block,

  • P_{x+1} must definitely be increased.
    • This might cause some of indices x+2, x+3, \ldots to require an increase too, in a sort of chain reaction.
  • P_y definitely cannot be increased: if it is, then even if P_{y+1} is increased the array cannot remain sorted.

So, there must exist some index m (x+1 \leq m \lt y) such that exactly the elements at indices x+1, x+2, \ldots, m are increased, and the rest aren’t.
Note that for the array to be sorted after this, P_m+K \leq P_{m+1} should hold, that is, K \leq P_{m+1} - P_m.

So, let d be the maximum adjacent difference within this block, i.e,
d = \max \{P_{m+1} - P_m : m \lt x \lt y \}
Clearly, if K \gt d then there’s no way to make this block sorted, while if K \leq d it’s always possible (we’ll definitely stop increasing once we reach whichever pair of indices have a difference of d).
This gives us an upper bound on K.

Note that this computation can be done separately for each increasing block in P, and the true upper bound is the minimum of all the d obtained this way.
Let this upper bound be R.


We now have a lower bound L and an upper bound R.
Given how they were obtained, it’s not hard to see that P is K-sortable for every K between L and R.
So, the answer is simply the sum of all the integers between L and R.
Note that if L \gt R, no valid K exists, and the answer is 0.

TIME COMPLEXITY:

\mathcal{O}(N) per testcase.

CODE:

Author's code (C++)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF (int)1e18

mt19937_64 RNG(chrono::steady_clock::now().time_since_epoch().count());

int solve(int n, vector <int> p){
    // so there are 3 types of indices 
    // fixed + prefix max
    // prefix max 
    // otherwise 
    
    vector <bool> fx(n, false);
    vector <bool> add(n, true);
    int mx = -1;
    for (int i = 0; i < n; i++){
        if (p[i] > mx){
            add[p[i]] = false;
            if (p[i] == i){
                fx[i] = true;
            }
            mx = p[i];
        }
    }
    
    vector <int> a(n);
    iota(a.begin(), a.end(), 0);
    
    int lk = 0, rk = n;
    for (int i = 1; i < n; i++){
        if (fx[i]) continue;
        if (add[p[i]] != add[p[i - 1]]){
            if (add[p[i]] == true){
                int mn = p[i - 1] - p[i];
                lk = max(lk, mn);
            } else {
                int mx = p[i] - p[i - 1];
                rk = min(rk, mx);
            }
        } else if (p[i] < p[i - 1]){
            return 0;
        }
    }
    
    if (lk > rk){
        return 0;
    }
    
    vector <int> prevadd(n, -n);
    int last = -n;
    for (int i = 0; i < n; i++){
        prevadd[i] = last;
        if (add[i]){
            last = i;
        }
    }
    
    vector <int> nxadd(n, 2 * n);
    last = 2 * n;
    for (int i = n - 1; i >= 0; i--){
        nxadd[i] = last;
        if (!add[i] && !fx[i]){
            last = i;
        }
    }
    
    vector <int> ac(n, n);
    last = -n;
    for (int i = 0; i < n; i++){
        if (fx[i]){
            ac[i] = i - prevadd[i] + 1;
            if (last != -n){
                ac[i] = min(ac[i], max(ac[last], i - last + 1));
            }
            last = i;
        }
    }
    
    for (int i = 0; i < n; i++){
        if (fx[i]){
            int kk = max(ac[i], nxadd[i] - i + 1);
            rk = min(rk, kk - 1);
        }
    }
    
    if (lk > rk) return 0;
    
    int ans = 0;
    for (int i = lk; i <= rk; i++){
        ans += i;
    }
    
    return ans;
}

void Solve() 
{
    int n; cin >> n;
    vector <int> p(n);
    for (auto &x : p) cin >> x, x--;
    
    cout << solve(n, p) << "\n";
}

int32_t main() 
{
    auto begin = std::chrono::high_resolution_clock::now();
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t = 1;
    // freopen("in",  "r", stdin);
    // freopen("out", "w", stdout);
    
    cin >> t;
    for(int i = 1; i <= t; i++) 
    {
        //cout << "Case #" << i << ": ";
        Solve();
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
    cerr << "Time measured: " << elapsed.count() * 1e-9 << " seconds.\n"; 
    return 0;
}
Tester's code (C++)
#include <bits/stdc++.h>
using namespace std;
#ifdef tabr
#include "library/debug.cpp"
#else
#define debug(...)
#endif

#define IGNORE_CR

struct input_checker {
    string buffer;
    int pos;

    const string all = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const string number = "0123456789";
    const string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const string lower = "abcdefghijklmnopqrstuvwxyz";

    input_checker() {
        pos = 0;
        while (true) {
            int c = cin.get();
            if (c == -1) {
                break;
            }
#ifdef IGNORE_CR
            if (c == '\r') {
                continue;
            }
#endif
            buffer.push_back((char) c);
        }
    }

    string readOne() {
        assert(pos < (int) buffer.size());
        string res;
        while (pos < (int) buffer.size() && buffer[pos] != ' ' && buffer[pos] != '\n') {
            assert(!isspace(buffer[pos]));
            res += buffer[pos];
            pos++;
        }
        return res;
    }

    string readString(int min_len, int max_len, const string& pattern = "") {
        assert(min_len <= max_len);
        string res = readOne();
        assert(min_len <= (int) res.size());
        assert((int) res.size() <= max_len);
        for (int i = 0; i < (int) res.size(); i++) {
            assert(pattern.empty() || pattern.find(res[i]) != string::npos);
        }
        return res;
    }

    int readInt(int min_val, int max_val) {
        assert(min_val <= max_val);
        int res = stoi(readOne());
        assert(min_val <= res);
        assert(res <= max_val);
        return res;
    }

    long long readLong(long long min_val, long long max_val) {
        assert(min_val <= max_val);
        long long res = stoll(readOne());
        assert(min_val <= res);
        assert(res <= max_val);
        return res;
    }

    vector<int> readInts(int size, int min_val, int max_val) {
        assert(min_val <= max_val);
        vector<int> res(size);
        for (int i = 0; i < size; i++) {
            res[i] = readInt(min_val, max_val);
            if (i != size - 1) {
                readSpace();
            }
        }
        return res;
    }

    vector<long long> readLongs(int size, long long min_val, long long max_val) {
        assert(min_val <= max_val);
        vector<long long> res(size);
        for (int i = 0; i < size; i++) {
            res[i] = readLong(min_val, max_val);
            if (i != size - 1) {
                readSpace();
            }
        }
        return res;
    }

    void readSpace() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == ' ');
        pos++;
    }

    void readEoln() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == '\n');
        pos++;
    }

    void readEof() {
        assert((int) buffer.size() == pos);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    input_checker in;
    int tt = in.readInt(1, 5e4);
    in.readEoln();
    int sn = 0;
    while (tt--) {
        int n = in.readInt(1, 5e5);
        in.readEoln();
        sn += n;
        auto p = in.readInts(n, 1, n);
        in.readEoln();
        assert((int) set<int>(p.begin(), p.end()).size() == n);
        int low = 0, high = n;
        vector<int> t;
        for (int i = 1; i < n; i++) {
            if (p[i - 1] > p[i]) {
                t.emplace_back(i);
            }
        }
        int sz = (int) t.size();
        for (int i = 0; i < sz; i++) {
            low = max(low, p[t[i] - 1] - p[t[i]]);
            if (i + 1 < sz) {
                int mx = 0;
                for (int j = t[i]; j < t[i + 1]; j++) {
                    mx = max(mx, p[j + 1] - p[j]);
                }
                high = min(high, mx);
            }
        }
        long long ans = 0;
        for (int k = low; k <= high; k++) {
            ans += k;
        }
        cout << ans << '\n';
    }
    cerr << sn << endl;
    assert(sn <= 5e5);
    in.readEof();
    return 0;
}
Editorialist's code (Python)
def solve(p):
    n = len(p)
    lo, hi = 0, n
    for i in range(1, n):
        if p[i-1] > p[i]:
            lo = max(lo, p[i-1] - p[i])
            if i+1 < n and p[i] > p[i+1]: return 0
    
    cur = n
    for i in range(1, n):
        if p[i] > p[i-1]:
            cur = max(cur, p[i] - p[i-1])
        else:
            hi = min(hi, cur)
            cur = 0
    if lo <= hi: return hi*(hi+1)//2 - lo*(lo-1)//2
    return 0

for _ in range(int(input())):
    n = int(input())
    p = list(map(int, input().split()))
    print(solve(p))
2 Likes

[Alternative of third part of solution]
If you know, whether a given k will k-sort the array or not and you have already found the lower bound as mentioned in the editorial. Then you can just do a binary search to find the upper bound. Check function of binary search can easily be written as you already know how to check for a given k, array is k-sortable or not.

5 Likes

Why is binary search correct - as in, how do you know that all the valid K will form a contiguous range?

No formal proof I have thought of, but since you know the elements which must be added and thus must be selected in the subset, you can’t arbitrarily add some large number to it. There will be some upper bound beyond which the process of increasing ai will actually make ai greater than the number which must not be included in the subset (i.e. if aj > aj+1, then I am referring to aj). [Ignore the case where array is already sorted]

Yep, that’s why I asked - my proof of the valid K forming a range relies on the the way the upper bound is found, so I was interested in knowing whether an alternate proof existed.
I think if you extend your thought to a proper proof, you’ll end up with the same construction of an upper bound (and hence won’t need binary search anymore).

Guesswork without a formal proof is fine in a contest but it’s not something I can put as the intended solution :smile:

Hello @iceknight1093, can you share some test case(s), over which i can dry run the solution to understand better. As i am not able to come up with test cases over which i can get complete clarity.