【题目来源】
https://mp.weixin.qq.com/s/Jz40LuAMUgXxfPwkQbW48A
【题目描述】
XiaoC 和 XiaoT 在数学课上无聊炸了 ……
他们玩起了游戏 ……
XiaoT 在纸上写下了一个 n 位数,而 XiaoC 则要从中删除 k 个,使得剩余的数字按原来的顺序排列得到的数字最大。
求求你帮帮他吧!
【输入格式】
第一行两个整数 n,k(1<=k<n<=500,000)。
接下来一行为一个 n 位数,保证不含前导 0。
【输出格式】
输出仅一行整数,表示删除 k 个数后可能得到的最大的数。
【输入样例】
4 2
1924
【输出样例】
94
【算法分析】
● 从下标1处开始输入字符串的一种 C++ 语法:https://blog.csdn.net/hnjzsyjyj/article/details/133217375
const int N=1e5+5; char str[N]; scanf("%s",str+1);
● 单调队列与滑动窗口
https://blog.csdn.net/hnjzsyjyj/article/details/126381659
https://blog.csdn.net/hnjzsyjyj/article/details/109680717
单调队列是指在队尾入队出队,在队首出队,且其元素具有单调性的特殊队列。其中,在队尾入队出队的操作是用来维护单调队列的单调性,在队首出队的操作是用来维护单调队列的大小。单调队列常用于动态规划问题的优化。
在实践中,单调队列或者用数组模拟实现,或者用STL中的deque实现,不能用STL中的queue实现。
特别注意,单调队列里存放的是原始序列的元素下标,而不是元素值。这是因为我们无法用元素值来判断元素是否过期。但是,我们在下文中谈论元素大小时,指的不是原始序列的元素下标的大小,而是元素下标在原始序列中对应的元素值的大小。
单调队列求最值时的进出队规则,可助记如下:
★ 构建单调递减队列的目的在于求最大值,求最大值时的操作为“大覆盖,小附加”(即:原始序列的待操作的元素,若比单调队列的队尾元素大,则按从单调队列的队尾元素向队首元素的方向,依序覆盖掉所有比原始序列的待操作的元素小的元素,直至遇到比原始序列的待操作的元素大的元素后终止;原始序列的待操作的元素,若比单调队列的队尾元素小,则附加到单调队列的队尾元素后面成为新的队尾元素)
★ 构建单调递增队列的目的在于求最小值,求最小值时的操作为“小覆盖,大附加”(即:原始序列的待操作的元素,若比单调队列的队尾元素小,则按从单调队列的队尾元素向队首元素的方向,依序覆盖掉所有比原始序列的待操作的元素大的元素,直至遇到比原始序列的待操作的元素小的元素后终止;原始序列的待操作的元素,若比单调队列的队尾元素大,则附加到单调队列的队尾元素后面成为新的队尾元素)
● 单调队列实例 → 洛谷 P1886:https://www.luogu.com.cn/problem/P1886
若给定滑动窗口大小为3,原始序列为{1, 3, -1, -3, 5, 3, 6, 7},则滑动窗口求最值问题利用单调队列模拟的执行过程如下图所示:
*** P1886 算法代码一 ***
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int q1[maxn],q2[maxn];
int a[maxn];
int n,k;
void minque() {
int h=1,t=0;
for(int i=1; i<=n; i++) {
while(h<=t && k<=i-q1[h]) h++;
while(h<=t && a[i]<=a[q1[t]]) t--;
q1[++t]=i;
if(i>=k) {
printf("%d ",a[q1[h]]);
}
}
}
void maxque() {
int h=1,t=0;
for(int i=1; i<=n; i++) {
while(h<=t && k<=i-q2[h]) h++;
while(h<=t && a[i]>=a[q2[t]]) t--;
q2[++t]=i;
if(i>=k) {
printf("%d ",a[q2[h]]);
}
}
}
int main() {
scanf("%d%d",&n,&k);
for(int i=1; i<=n; i++) {
scanf("%d",&a[i]);
}
minque();
cout<<endl;
maxque();
return 0;
}
/*
in:
8 3
1 3 -1 -3 5 3 6 7
out:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
*/
*** P1886 算法代码二 ***
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int a[maxn];
deque<int> q; //q存放编号
int n,k;
void que_min() {
q.clear();
for(int i=1; i<=n; i++) {
while(!q.empty() && i-k>=q.front()) q.pop_front();
while(!q.empty() && a[i]<=a[q.back()]) q.pop_back();
q.push_back(i);
if(i>=k) cout<<a[q.front()]<<" ";
}
}
void que_max() {
q.clear();
for(int i=1; i<=n; i++) {
while(!q.empty() && i-k>=q.front()) q.pop_front();
while(!q.empty() && a[i]>=a[q.back()]) q.pop_back();
q.push_back(i);
if(i>=k) cout<<a[q.front()]<<" ";
}
}
int main() {
cin>>n>>k;
for(int i=1; i<=n; i++) cin>>a[i];
que_min();
cout<<endl;
que_max();
return 0;
}
/*
input:
8 3
1 3 -1 -3 5 3 6 7
output:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
*/
● 本题算法分析
n 个数字要删除 k 个数字,保留 n-k 个数字,使得字典序最大。
如果第 1 个数字在第 i 位产生,第 2 个数字肯定在 i+(1~(k+2)) 中产生,选择这部分最大的数字作为第 2 位。依次类推做到第 n-k 个,所得即为答案。显见,此为贪心策略。
具体在做题过程中,可以发现区间整体向右偏移,故可以用单调队列辅助来做,即经典的滑动窗口题目求区间最大最小值。
【算法代码】
#include<bits/stdc++.h>
using namespace std;
const int M=5e5+5;
char s[M];
deque<int> q;
int n,k;
int main() {
scanf("%d%d",&n,&k);
scanf("%s",s+1);
for(int i=1; i<=n; i++) {
while(q.size() && s[q.back()]<s[i]) q.pop_back();
q.push_back(i);
if(i>=k+1) {
printf("%c",s[q.front()]);
q.pop_front();
}
}
return 0;
}
/*
in:
4 2
1924
out:
94
*/
【参考文献】
https://blog.csdn.net/hnjzsyjyj/article/details/109680717
https://blog.csdn.net/hnjzsyjyj/article/details/126381659
https://mp.weixin.qq.com/s/Jz40LuAMUgXxfPwkQbW48A
https://blog.csdn.net/hnjzsyjyj/article/details/133217375