https://codeforces.com/gym/102433
题意
给定一个长度为
n
n
n 的数组
a
[
]
a[]
a[],整数
m
m
m。
保证对于 1 ~ m 中的所有数都会出现在这个数组中至少一次。
找到字典序最小的子序列,使得 1 ~ m 中的所有数出现且仅出现一次。
1
≤
k
≤
n
≤
200000
1 ≤ k ≤ n ≤ 200 000
1≤k≤n≤200000
1
≤
x
i
≤
k
.
1 ≤ x_i ≤ k.
1≤xi≤k.
思路
从前往后对于每个位置来说,假设当前位置 i
拿了放在子序列中,需要判断之前拿过的数:
- 如果之前拿过比
a[i]
大的数x
,并且x
在后面的位置也出现,为了使得所拿数的字典序尽可能小,那么之前拿的x
就可以丢掉,到后面的时候再拿; - 如果之前拿过比
a[i]
大的数x
,但是x
在后面的位置没有出现,那么x
必拿不可。
但是如果对于每个位置都遍历之前拿过的数,复杂度是 O(n^2),需要优化。
对于上一位置 i
判断了之前拿过的数,那么当前位置 j
:
- 需要向前依次判断拿过的数,但可以在下面的条件下停止:
- 碰见一个比
a[j]
小的数x
(前面的数已经被x
判断过了,不用再判断),或者 - 碰见一个比
a[j]
大的数x
,但是x
在后面的位置没有出现,这个x
必拿。(此时x
前面拿的数 要么大于x
,已经被x
判断过了不用判断;要么小于x
,但如果丢掉之后x
必将前移,使得子序列字典序变大,那么都不能丢,都不用判断。所以这种情况下停止往前判断)
- 碰见一个比
- 然后拿上当前元素。(每次都把当前位置拿上,不优的话后面判断时会丢)
在这种判断方式下,所有拿过的数只会被判断一次,O(n),并且保证了所拿子序列字典序最小。
可以用一个栈来实现上述判断,栈中依次存放要拿的子序列中的数。
遍历所有的位置,对于每个位置:
- 如果当前元素
x
在前面已经拿过,跳过; - 依次向前判断拿过的数,如果栈顶元素大于等于当前元素,并且栈顶元素在后面的位置还出现,那么就可以丢掉栈顶元素;
- 拿上当前元素,把当前元素入栈;
for(int i=1;i<=n;i++)
{
int x = a[i];
if(mp[x]) continue;
while(stk.size() && a[stk.top()] >= x && last[a[stk.top()]] > i) mp[a[stk.top()]] = 0, stk.pop();
stk.push(i);
mp[a[i]] = 1;
}
最终栈中元素就是所选子序列元素。
Code
//https://codeforces.com/gym/102433
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define endl '\n'
map<int, int> mp;
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], last[N];
int flag[N];
signed main(){
Ios;
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i], last[a[i]] = i;
stack<int> stk;
for(int i=1;i<=n;i++)
{
int x = a[i];
if(mp[x]) continue;
while(stk.size() && a[stk.top()] >= x && last[a[stk.top()]] > i) mp[a[stk.top()]] = 0, stk.pop();
stk.push(i);
mp[a[i]] = 1;
}
vector<int> ans;
while(stk.size()) ans.push_back(stk.top()), stk.pop();
for(int i=ans.size()-1;i>=0;i--) cout << a[ans[i]] << " ";
return 0;
}
代码很简单,但是想清楚思路还是要花点时间的。