一、概述
输入一个字符串,删除其中重复的字符,并保证剩余的元素排列是所有可能性中最小的。
这什么意思?
以cbacdcbc为例,删完重复的,剩下abcd四个字符,那么我可以这样删:
c b a c d c b c
c b a x d x x x
结果就是cbad
也可以这样删
x x a c d x b x
结果就是acdb,这个字典序比cbad小,要这个。
二、分析
麻烦。很复杂。我自己分析的贪心算法都只能找到局部最优,找不到全局最优。
先想一个问题,我们只遍历一遍的话,如何判断当前的字符放不放到结果中?
以字符串cbacdcbc为例
首先是c,放入结果,结果为c
然后是b,放入结果,结果为cb
然后是a,放入结果,结果为cba
然后是c,结果中已经有c了,那这两个c要哪个呢?要前面那个是cba,后边这个是bac,那肯定要后边这个不要前边这个
那我这么看太麻烦了不说,还有这种情况:
对于字符串bcabc
首先是bca,结果是bca,
然后是b,结果中已经有b了,那要之前的b还是这个b?要之前的b,结果是bca,这个b,结果是cab,要之前的。
然后是c,结果中已经有c了,那要之前的c还是这个c?要之前的c,结果是bca,这个c,结果是bac,要这个。
所以结果是bac。结果错了。
应该是abc。为什么?因为我们应该要第二个b而不是第一个。
也就是说,不能只遍历一次,只遍历一次只能得到局部最优,无法得到全局最优。
正确算法是遍历两次。第一次知道各个字符的储量。仍然以cbacdcbc为例。
先遍历一次探明储量。a有一个,b有2,c有4,d有1。
然后遍历第二次:
首先是c,放入结果,结果为c
然后是b,先看结果,结果中有c,现在有b,是委屈一下b,把b放在c后面,结果是bc,还是不要这个c了,结果仍然是b?
也就是说,这个c到底要不要?那得看后面有没有c,后面还有c,那么必定最后有结果…b…c;后面没有c,那么这个独苗就得保住。之前已经探明了储量,有c,那么这个c就滚蛋,目前结果是b。
总结起来就是:保这个c,结果是cb…,不保,结果是b…c…。我们要字典序最小,那么肯定要后者。
之后是a。同理,由于b比a大,是委屈这个a,结果是ba,还是不要这个b,结果是a。看b的储量。b有俩,那不客气了,b滚蛋
结果为a
之后是c,尾巴是a,比c小,放入结果,结果是ac。
之后是d,尾巴是c,放入结果,结果是acd。
之后是c,结果中已经有c了,那么这个c就不用看了。因为结果中一定是当前最优的。
之后是b,尾巴是d,比b大,想删,只有一个,删不了,那b就只能放在最后,结果是acdb。
以此类推。
总结一下:先探明各个字符的储量。然后遍历一次想得到结果。就得拿捏好到底要哪个字符。我们要求字符序越小越好,也就是说小的字符越在前越好。
那么,每当遇到一个结果中没有的字符,就从最后一个当前的结果开始比,最后一个字符比当前字符大,储量还有,那么就把它删了,否则就把当前字符放在最后。
核心就是b…c…一定比cb好。那么我就把cb这个c撇了,反正之后还有。优化结果更重要。
先保证有无,再保证字典序最小。
三、总结
有点晕,这个算法不太好想,得明确贪心的规则:
当前字符在结果中已存在?
是,跳过;否,结尾字符储量还有?
否,当前字符放在最后;是,当前字符比最后一个字符小?
否,当前字符放到最后;是,删除最后一个,继续和最后一个比较。
PS:代码如下:
class Solution {
public:
string removeDuplicateLetters(string s) {
string res="";
vector<int> HT(256,0);
for(auto i:s)
HT[i]++;
for(auto i:s)
{
HT[i]--;
if(res=="")
{
res+=i;
continue;
}
else if(res.find(i)!=string::npos)
continue;
else
{
while(res!=""&&HT[res[res.size()-1]]>0&&res[res.size()-1]>i)
res.pop_back();
if(res.find(i)==string::npos)
res+=i;
}
}
return res;
}
};