贪心算法刷题

605. 种花问题

        很简单的一道题,感觉没什么说的,我的做法是检查每一个位置是否可以种花,若已经有花,直接跳过,若无花,检查前一个和后一个位置,若都空,则在当前位置种花,ans++,对于开始和结尾单独处理

    bool canPlaceFlowers(vector<int>& flowerbed, int n) {
        int ans=0,len=flowerbed.size();
        if(len==1) return 1-flowerbed[0]>=n; //对于长度为1的特殊处理
        for(int i=0;i<len;i++){
            if(flowerbed[i]) continue;
            if(i==0){
                if(flowerbed[1]==0)
                    ans++,flowerbed[0]=1;
            }else if(i==len-1){
                if(flowerbed[len-2]==0)
                    ans++,flowerbed[len-1]=1;
            }else if(flowerbed[i]==0&&flowerbed[i-1]==0&&flowerbed[i+1]==0)
                ans++,flowerbed[i]=1;
        }
        return ans>=n;
    }

        题解给了另一种思路,即查看两次相邻的有花的花坛的距离,这里可种的花数为距离数-2后除以2下取整,pre记录上一次种花的位置,初始化为-1,在花坛数组的遍历中,遇到有花的位置后,计算这一0区间可以种花的数目,同时更新pre=i.走出循环后还需考虑两种特殊情况,一是花坛数组末尾全是0,需要再计算这一结尾0区间可以种花的数目,二是整个花坛数组全都是0,需要单独计算.在这里我们也可以看出其实对于开头的0数组,我们是采用了pre初始化为-1来进行处理.

680. 验证回文串 II

        题目描述很简单,看数据量达到10^5,说明我们需要想一个O(n)的算法.

        那么我们先来看看对于一个一般的字符串,我们是怎么判断它是不是回文串的,通常的做法是使用双指针i,j从开始和末尾进行判断,s[i]==s[j],i++,j--,当走到某一处位置,不相等了,说明他不是回文串,而对于本题,因为我们可以删除一个元素,也就是说我们有一次的容错空间,当不相等了,我们可以删除s[i],即i++,继续判断或者删除s[j],即j--继续判断.

        这里的贪心体现在,我们尽可能地去寻找当前已经匹配上的首尾子串

    bool validPalindrome(string s) {
        int i=0,j=s.size()-1;
        while(s[i]==s[j]&&i<j)    i++,j--;
        if(i<j){
            int i1=i,j1=j;
            i++;
            while(s[i]==s[j]&&i<j) i++,j--;
            if(i>=j) return true;
            j1--;
            while(s[i1]==s[j1]&&i1<j1) i1++,j1--;
            if(i1>=j1) return true;
        }else return true;
        return false;
    }

1053. 交换一次的先前排列

        题意:一次交换,要得到字典序小于原来的最大可能

        因为我们要使得字典序变小所有我们肯定要交换arr[i]与他后面的arr[j] (i<j,arr[i]>arr[j]),而对于最大这一要求,需要我们(1)使得i尽可能大,也就是使得交换尽可能发生在靠后的位置,同时我们要使得(2)arr[j]在满足小于arr[i]这一要求下,尽可能大,这样使得,换过去的arr[j]比较大同时(3)对于相等的arr[j],我们要使得j的下标尽可能小,这样对于交换过来的更大一点的arr[i]才能更加靠前一点

        对于(1)(2)(3)这三点要求,我们是有优先级的,先满足1,然后2,然后3,这里如果我们看得更细一点的话,会发现,对于1,我们是找到从后往前找一个逆序对((i<i+1,arr[i]>arr[i+1])),也就是从后往前找第一个不满足随着下标增大,arr存储的数值也增大的序列,但是在i之后,都是满足随着下标增大,arr存储的数值也增大的,这就为2,3要求简化了思维量,我们只用从最后开始找一个小于当前arr[i]并且与相等元素相比下标最小的最大数值的arr[j]即可

    vector<int> prevPermOpt1(vector<int>& arr) {
        for(int i=arr.size()-2;i>=0;i--){
            if(arr[i]>arr[i+1]){
                int j=arr.size()-1;
                for(;arr[j]>=arr[i]||arr[j]==arr[j-1];j--);
                swap(arr[i],arr[j]);
                return arr;
            }
        }
        return arr;
    }

     135.分发糖果  

        题意理解: 题意比较明晰,不过样例表明如果两人分数相同则对他们两人的糖果数没有约束.

        思路一:

        思路分析: 因为它的限制条件比较弱(只在临近元素中有比较),比较容易想到的思路是进行两次方向的排查,先从左往右遍历,求出满足当前方向的对于每个人应该分发的糖果数目left[i],再从从右往左遍历,对应地求出right[i],对于某一个人,他所获得的糖果数就是max(left[i],right[i])

        对于left[i]的求解方法如下:第一个人发一个糖果,之后对于每个人,如果此人分数高于前一个人,那么他得到的糖果就是前一个人糖果加1;如果此人分数小于或者等于前一个人,那么他得到一个糖果.right[i]同理.     时间复杂度空间复杂度均为O(n)

        代码如下:

    int candy(vector<int>& ratings) {
        int len=ratings.size(),left[len];
        left[0]=1;
        for(int i=1;i<len;i++)
            if(ratings[i]>ratings[i-1])
                left[i]=left[i-1]+1;
            else left[i]=1;
        for(int i=len-2;i>=0;i--)
            if(ratings[i]>ratings[i+1]&&left[i+1]+1>left[i])
                left[i]=left[i+1]+1;
        int sum=0;
        for(int i=0;i<len;i++)
            sum+=left[i];
        return sum;
    }

        注: 这里我们为了节省right数组,只在需要+1的时候才更新left数组里面存放的数值,其他情况显然都是left数组更大一些.

        思路二:

        思路讲解:其实我们也可以写出一次遍历的算法,时间复杂度O(n),空间复杂度O(1).         只要我们以递增递减作为变化依据,如果当前分数大于上一个,说明处在递增序列里面,他所分的糖果为上一个人加1,而如果当前分数小于上一个,说明处在递减序列里面,他所分的糖果为1,但是为了保证题目要求,需要将之前处在递减序列里面的所有元素+1,当然我们不用真的加1,只需要维持des表示当前递减序列的长度即可,这个时候细心的小伙伴可能就会发现了,对于 3 4 5 6 7 2 1的例子和 1 2 3 2 1的例子,前者不需要把7列入递减序列(加上des-1),而后者需要把3列入递减序列(加上des),这个又该怎么区分呢?        如果我们整体地来看一看,会发现这是取决于上一个递增序列的长度和本递减序列的长度的大小关系,如果上一个递增序列的长度大于递减序列的长度,则不需要把本递减序列第一个元素第一个列入(加上des-1即可),而如果小于等于,则需要列入(加上des).

        也就是说,我们只需要维护asc上一次递增序列的长度,des本次递减序列的长度,pre分配给上一个同学的糖果数这三个变量即可.

        代码如下:

    int candy(vector<int>& ratings) {
        //如果当前第i个人评分比第i-1个人评分高,
        int len=ratings.size(),des=0,asc=0,pre=1,ans=1;
        bool back;
        for(int i=1;i<len;i++){
            int cur;
            if(ratings[i]>ratings[i-1]){//升序
                if(back) asc=0,back=false;
                cur=pre+1;
                asc++;des=0;
            }else if(ratings[i]==ratings[i-1]){
                cur=1;des=0;asc=0;
            }else{
                back=true;
                des++;
                cur=1;
                if(des<=asc) ans+=des-1; else ans+=des; 
            }
            ans+=cur;
            pre=cur;
        }
        return ans;
    }

        注: 1. 如果相邻rating等于,则不存在限制关系,des=ans=0;

              2. 对于上述算法,我们还需要一个bool back来确定什么时候更新asc=0;显然我们只有在再次进入递增序列时,才能更新上一个存储的递增序列长度asc=0

763. 划分字母区间

        把一个字符串划分成尽可能多的块,使得同一个字母最多出现在一块里面

        如果我们有跳跃游戏那一题的经验我们就会想到,在s[i](假设是字符'a')处,用arr[i]记录下一次出现s[i](字符'a')的下标,这样对于s[i],我们就要从i走到arr[i],这里面的每一个元素也是同样的操作,如果发现了更大的arr[i],则更新此处小循环的边界,而每走一个小循环就说明是ans++; 

        比如样例二,eccbbbbdec,就是829456-1-1-1-1,对于没有后继元素的,我们记为-1,对于s[0],arr[0]是8,所以我们要从s[0]走到s[8],当走到s[2]时,arr[2]=9>8,更新循环边界.最后就只有一个区间,长度是10

    vector<int> partitionLabels(string s) {
        int len=s.size(),arr[len];
        map<char,int> map1;
        for(int i=len-1;i>=0;i--)
            if(map1.count(s[i])){
                arr[i]=map1[s[i]];
                map1[s[i]]=i;
            }else{
                arr[i]=-1;
                map1[s[i]]=i;
            }
        int i=0,k=0;vector<int> ans;
        while(1){
            for(int j=arr[i];i<=j;i++){
                j=max(j,arr[i]);
            }
            if(i==k) i++; //如果i==k,说明上次没走小循环(即不满足i<=arr[i]),这只在arr[i]==-1才会发生,此时需要i++跳出这个死循环
            ans.push_back(i-k);
            k=i;
            if(i==len) break;
        }
        return ans;
    }

    注:  1.前面我们使用map从后往前辅助构建数组arr[i],当然也可以直接使用int temp[26]作为辅助.

           2.因为我们使用-1作为没有相同后继的标志,这样在j=max(j,arr[i])时不会错误更新j,但是同时也引进了新问题,也就是当出现没有后继的单个元素,会陷入死循环,为此我们新增判断条件: if(i==k) i++; //如果i==k,说明上次没走小循环(即不满足i<=arr[i]),这只在arr[i]==-1才会发生,此时需要i++跳出这个死循环

        当然其实还有一种更好的写法,如果我们注意到其实这就是一次对于s字符串的遍历,我们可以记录每次的start,end表示一个区间,那么每次i走到end就表示区间结束,ans.push_back(end-start+1),这时也要将start更新为end+1直到i走到s的末尾,得到的ans就是所求.

        不过因为我们记录为没有后继为-1,所以我们还要处理一下i>end(arr[i]=-1)的情况

class Solution {
public:
    vector<int> partitionLabels(string s) {
        int len=s.size(),arr[len];
        map<char,int> map1;
        for(int i=len-1;i>=0;i--)
            if(map1.count(s[i])){
                arr[i]=map1[s[i]];
                map1[s[i]]=i;
            }else{
                arr[i]=-1;
                map1[s[i]]=i;
            }
        int start=0,end=0;vector<int> ans;
        for(int i=0;i<s.size();i++){
            end=max(end,arr[i]);
            if(i==end){
                ans.push_back(end-start+1);
                start=end+1;
            }else if(i>end){
                ans.push_back(i-end);
                end=i;
                start=end+1;
            }
        }
        return ans;
    }
};

        如果我们跳出下一后继这一思想,对于出现过的字母,我们记录他们最后出现的下标i就会简便许多

    vector<int> partitionLabels(string s) {
        int len=s.size(),arr[26];
        for(int i=0;i<s.size();i++)
            arr[s[i]-'a']=i;
        int start=0,end=0;vector<int> ans;
        for(int i=0;i<s.size();i++){
            end=max(end,arr[s[i]-'a']);
            if(i==end){
                ans.push_back(end-start+1);
                start=end+1;
            }
        }
        return ans;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值