1. 程式人生 > 實用技巧 >[USACO18DEC]Sort It Out P

[USACO18DEC]Sort It Out P

初看本題毫無思路,只能從特殊的 \(K = 1\) 出發。

但是直接考慮構造一組字典序最小的方案還是不好構造,可以考慮先手玩一下樣例。通過自己手玩的樣例可以發現,貌似沒有被選出來的數在原排列中都是遞增的子序列。比如說 \(1 \ 6 \ 5 \ 3 \ 4 \ 2\) 可以證明 \(2 \ 5 \ 6\) 是字典序最小的方案,那麼剩下的數按照在原排列中的順序依次寫下是 \(1 \ 3 \ 4\) 是一個遞增序列。可以思考一下為什麼會有這樣的結論出現。

不難發現不論我們怎麼操作,原來序列中順序對的相對位置必然是不會變動的。比如說上方的 \(1 \ 3 \ 4\) 不論怎麼變換 \(1\) 都會在 \(3\)

之前,\(3\) 都會在 \(4\) 之前。並且你會發現將剩下的所有數按照字典序依次進行操作是能讓這個排列重新排好的。於是我們可以得到一個結論,原排列中的任意一個遞增序列保留下來剩下的作為一個操作集合都能時這個排列重新排好。

既然如此,為了讓操作集合字典序最小,相反我們需要讓保留集合字典序最大。因此保留集合就一定需要選擇最長遞增子序列中字典序最大的那個。可以考慮每次貪心地選取能拼成最長遞增子序列中的最大的元素即可,具體實現可以先求出 \(dp_i\) 表示以 \(i\) 位置結尾的最長遞增子序列長度。最後將每個位置按照 \(dp\) 值為第一關鍵字,按照 \(a_i\) 為第二關鍵字排序即可。

那麼回來思考原問題,字典序第 \(K\) 小的方案怎麼求。不難發現實際上我們還是要求保留集合第 \(K\) 大的方案。於是可以考慮從低到高位逐步確定選擇的集合,那麼我們就需要求出 \(dp_i\) 表示以 \(i\) 開頭的最長遞增子序列的數量。可以先考慮一個樸素的求法,令 \(f_i\) 為以 \(i\) 開頭的最長遞增子序列的長度,那麼會有轉移:

\[dp_i = \sum\limits_{f_i = f_j + 1, a_i < a_j, i < j} dp_j \]

實際上又因為 \(f_i = \max\limits_{a_i < a_j, i < j} f_j + 1\)

,實際上我們的 \(dp\) 就是求權值在一段字尾中 \(f\) 最大值的數量,這個可以直接在權值線段樹上實現,於是我們就將求 \(dp_i\) 的複雜度降至 \(O(n \log n)\)

那麼最後確定第 \(K\) 大的保留集合時,將所有 \(dp\) 值相同的位置壓入到一個 \(vector\) 當中並按照權值為第二關鍵字排序,依次確定每個位即可。

需要注意的是 \(dp_i\) 的數量可能會爆 \(long long\) 但因為我們只需要和 \(K\) 比大小所以需要隨時將 \(dp\) 值與 \(1e18\)\(\min\)

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ls (p << 1)
#define rs (p << 1 | 1)
#define mid (l + r >> 1)
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define dep(i, l, r) for (int i = r; i >= l; --i)
const int N = 100000 + 5;
const int inf = 1000000000000000001;
struct tree {
	int mx, cnt;
}dp[N], t[N << 2];
int n, k, len, ans, a[N], book[N];
vector <int> G[N];
int read() {
	char c; int x = 0, f = 1;
	c = getchar();
	while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
	while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
	return x * f;
}
tree up(tree x, tree y) {
	tree ans;
	if(x.mx > y.mx) ans = x;
	else if(x.mx < y.mx) ans = y;
	else ans.mx = x.mx, ans.cnt = min(x.cnt + y.cnt, inf);
	return ans;
}
void update(int p, int l, int r, int x, int y, tree k) {
	if(l >= x && r <= y) { t[p] = k; return;}
	if(mid >= x) update(ls, l, mid, x, y, k);
	if(mid < y) update(rs, mid + 1, r, x, y, k);
	t[p] = up(t[ls], t[rs]);
}
tree query(int p, int l, int r, int x, int y) {
	if(l >= x && r <= y) return t[p];
	tree ans; ans.mx = ans.cnt = 0;
	if(mid >= x) ans = up(ans, query(ls, l, mid, x, y));
	if(mid < y) ans = up(ans, query(rs, mid + 1, r, x, y));
	return ans;
}
bool cmp(int x, int y) {
	return a[x] > a[y];
}
signed main() {
	n = read(), k = read();
	rep(i, 1, n) a[i] = read();
	dep(i, 1, n) {
		dp[i] = query(1, 1, n, a[i], n);
		++dp[i].mx, dp[i].cnt = (dp[i].mx == 1 ? dp[i].cnt + 1 : dp[i].cnt);
		update(1, 1, n, a[i], a[i], dp[i]);
		G[dp[i].mx].push_back(i), len = max(len, dp[i].mx);
	}
	rep(i, 1, len) sort(G[i].begin(), G[i].end(), cmp);
	int P = 0; 
	dep(i, 1, len) {
		for (int j = 0; j < G[i].size(); ++j) if(G[i][j] > P && a[G[i][j]] > a[P]) {
			if(dp[G[i][j]].cnt < k) k -= dp[G[i][j]].cnt;
			else { P = G[i][j]; break;}
		}
		book[a[P]] = 1; if(P != n + 1 && !ans) ans = i; 
	}
	printf("%lld\n", n - ans);
	rep(i, 1, n) if(!book[i]) printf("%lld\n", i);
	return 0;
}

值得一提的是,在這種最優性問題毫無思路時,可以通過手玩樣例發現一些策略和性質。