链接:https://ac.nowcoder.com/acm/contest/551/D
题目描述
CSL 以前不会字符串算法,经过一年的训练,他还是不会……于是他打算向你求助。
给定一个字符串,只含有可打印字符,通过删除若干字符得到新字符串,新字符串必须满足两个条件: 原字符串中出现的字符,新字符串也必须包含。 新字符串中所有的字符均不相同。 新字符串的字典序是满足上面两个条件的最小的字符串。
输入描述:
仅一行,有一个只含有可打印字符的字符串 s。
|s|≤105
输出描述:
在一行输出字典序最小的新字符串。
示例1输入
bab
示例1输出
ab
示例2输入
baca
示例2输出
bac
备注:
ASCII字符集包含 94 个可打印字符(0x21 - 0x7E),不包含空格。
这道题我最初的想法是用优先队列贪心:
将每个字符出现的所有地点加入该字符的优先队列(位置靠前的优先),然后从字典序最小的字符开始,记录下该字符最靠前的地点(记为r)并清空后面所有地点,从上一次遍历的终点 l 遍历到这一次的终点 r ,如果该地点上的字符 t 在 r 后面还有出现,则 t 不会在该地点出现(pop);如果 t 最后出现的位置就是该地点,则记录下该地点。
最后从头到尾遍历所有地点,如果记录过该地点则输出该地点的字符。
贴上代码:
#include<bits/stdc++.h>
using namespace std;
char s[100010];
priority_queue <int,vector<int>,greater<int> > rem[300];
int vis[100010];
int main()
{
scanf("%s", s);
int len = strlen(s);
for(int i = 0; i < len; i++) rem[s[i]].push(i);
int l = 0, r;
for(int i = 0x21; i <= 0x7E; i++){
if(i != ' ' && !rem[i].empty()){
r = rem[i].top();
vis[r] = 1;
while(!rem[i].empty()) rem[i].pop();
for(int j = l; j < r; j++){
char t = s[j];
if(rem[t].size() > 1) rem[t].pop();
else if(rem[t].size() == 1){
vis[j] = 1;
rem[t].pop();
}
}
l = r + 1;
}
}
for(int i = 0; i < len; i++) if(vis[i]) printf("%c", s[i]);
return 0;
}
虽然这种想法这种代码简直完美,但这种思路是错的。
很显然,从字典序更小的字符最靠前地点(r)开始的贪心会受到 r 前序列的影响,这种影响不是简单的pop后面还会出现的位置记录是最后出现的位置能够消除的。比如说:输入bcab,按照上述思路输出的就会是cab,但显然正确输出应该是bca。
所以,正确思路应该是dp:
从位置 i = 0 开始,根据 i-1 前的最优解,寻找从 i-1连续往前字典序比s[i]大且后面还会出现的位置,并让 s[i] 去替换最靠前的位置。具体见代码:
#include<bits/stdc++.h>
using namespace std;
char s[100010];
int tol[300], cnt, v[300], ans[100];
int main()
{
cin >> s;
int len = strlen(s);
for(int i = 0; i < len; i++){
tol[s[i]]++;
}
for(int i = 0;i < len; i++){
int now = s[i];
tol[now]--;
if(v[now]) continue;
v[now] = 1;
while(ans > 0 && ans[cnt] > now && tol[ans[cnt]] > 0) v[ans[cnt--]] = 0;
ans[++cnt] = now;
}
for(int i = 1; i <= cnt; i++) cout << (char)ans[i];
return 0;
}
总结:再看似完美的思路都可能有bug。保持敬畏。