【BIT2021程设】11.卡牌游戏

写在前面:

本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有BIT的学弟学妹们在百度搜思路时翻到了这一条博客,也希望它能对你产生一点帮助(当然,依经验来看,每年的题目也会有些许的不同,所以不能保证每一题都覆盖到,还请见谅)。

不过本人由于学艺不精,代码定有许多不足之处,欢迎各位一同来探讨。

同时请未来浏览这条博客的学弟学妹们注意,对于我给出完整代码的这些题,仅作帮助大家理解思路所用(当然,因为懒,所以大部分题我都只给一个伪代码)。Anyway,请勿直接复制黏贴代码,小学期的作业也是要查重的,一旦被查到代码重复会严厉扣分,最好的方法是浏览一遍代码并且掌握相关的要领后自己手打一遍,同时也要做好总结和回顾的工作,这样才能高效地提升自己的代码水平。

加油!


成绩10开启时间2021年08月31日 星期二 12:30
折扣0.8折扣时间2021年09月3日 星期五 23:00
允许迟交关闭时间2021年10月10日 星期日 23:00

Description

小张在玩一种卡牌游戏,牌组由2n张牌组成,其中n张上写有数字1...n各一张,其余n张上全部是数字0。

现在牌组经过随机打乱后,小张拿走其中n张牌作为手牌,其余n张牌作为牌堆。

小张想经过若干次如下操作使得牌堆自顶向下的牌依次为1...n。

每一次操作,小张选择任意一张手牌放到牌堆底,并将牌堆顶的牌放入手牌。

他想知道最少进行几次操作,使得牌堆自顶向下的牌依次为1...n。

Input

第一行一个数n(1 \leq n \leq 200000 )

第二行n个数,表示小张手中的牌。

第三行n个数,表示牌堆,数组从左向右的顺序表示牌堆自顶向下的顺序。

Output

一个整数,表示最少执行的操作数。

测试用例 1以文本方式显示
  1. 3↵
  2. 0 2 0↵
  3. 3 0 1↵
以文本方式显示
  1. 2↵
1秒64M0
测试用例 2以文本方式显示
  1. 3↵
  2. 0 2 0↵
  3. 1 0 3↵
以文本方式显示
  1. 4↵
1秒64M0

题意分析:

        很有意思但不难的一道题。

        首先,总共2n的牌里有n张是0,而0是不参与最后的排序的,这说明什么呢?说明我们可以不需要付出交还手上已有的1~n这些牌(称为有效牌)为代价,就可以从牌库摸牌(因为手里有n张牌,若牌库里仍然有我们想要的牌,则我们手里必定会有0,这样我们只需要把0沉底,再从牌库顶摸牌就行)。这种“白嫖”的做法大大简化了我们的解题思路,这也正是这一题“贪心”的来源。我们就可以把整个操作流程分为两个阶段:

        ①若干次地将手中的0沉底,同时摸一张牌;

        ②在某个合适的时机,开始把手里的有效牌依次沉底,直到达到目标。

        可以证明,任意其他的策略都不如以上这个策略有效,这一定是最优策略(证明方法读者可以自行思考,不算太难,我放在这一部分的下方)。说是两个阶段,实际上的关键就是一个时间点,即——什么时候能开始将有效牌沉底。我们先思考较为普通的情况,即连续抽牌若干次,直到牌库满足了某个条件S,我们就开始沉底有效牌。那么这个条件S是什么呢?首先,1必须在手里,因为我们之前连续抽了若干次牌,牌库底必定是0,此时如果1不在手里,就无法开始排序;其次,2要么在手里,要么在牌库顶,因为我们在放完1之后马上就要放2,在这个过程中我们只有一次的抽牌机会,所以我们需要在一次之内让2进入手牌,所以2要么现在就在手里,要么下一抽正好进入手里……类似的,对于有效牌i,它都必须满足,要么在手里,要么在牌库从上数起的i-1张内,这就是我们想达到的“某种条件S”。那么要抽几次才能达到这种条件?我们就要看,离达到这种条件“最远”的那张牌“究竟有多远”,举个例子,n=5时,如果牌库自顶向下依次是2,4,1,0,3,我们知道,牌1需要三次操作能够满足条件S,牌2已然满足条件S,牌3需要三次操作能够满足条件S,牌4已然满足条件S,牌5在手上,也满足条件S,所以正式沉入有效牌之前,我们需要三次沉底0,这时候牌库自顶向下依次是0,3,0,0,0,而我们手牌中则有1,2,0,4,5,这时候连续放入1,2,抽两张牌正好可以把3抽到手里,再连续放入3,4,5,这样我们就用3+5=8次完成了全部操作,根据我们证明出来的贪心策略,这就是最优解。

        然而以上我们讨论的只是最一般的情况,有没有特殊情况呢?当然有,最最特殊的情况就是——牌库自顶向下已经帮我们排好1~n了,那我们啥也不用做就得到了最终结果;差一点的情况是已经帮我们排好了1~n-1,我们只要美滋滋地扔进去一张n,就大功告成; 最让人烦恼的情况是,从第二张到牌库底依次为2~n,而1还孤零零地在手上,或者已经帮你排好了1~k-1,但k不在手上,此时就算他已经帮你完成了99%也无济于事,依然要全部推翻、从头再来。总结一下,什么情况下我们可以半路插队,直接进入操作②呢?应该要满足两个条件,其一是牌库自底向上依次是k~1,其中1\leq k\leq n,其二是当前的手牌和牌库能够满足条件S',使得在连续投入有效牌的过程中不会因为没牌而间断。条件二的判定也很简单,与之前完全类似,如果牌库底已经从1排到了k,那么这时候k+1必须在手中,k+i则必须在牌库顶向下的前i-1张内。这时候就可以放心大胆地投入有效牌啦。


贪心策略的证明:

        用反证法,先证②是必要的,那么我们就假设我们偏不按②来,即在投入若干张有效牌的过程中间沉底一张“0”——这时候发牌姬都恨不得骂你一句“XX”。因为你把已经完成的牌库底的部分序列成功破坏了,得,一切从头再来,很显然不行。因此一旦开始投入有效牌,就不可能沉底0。

        再来证明①,如果我们偏不按①来,在沉若干张0的过程中沉入有效牌会怎样?分两种情况,如果这时候你的所有有效牌1~n都已经到手上,那么这时候就已经可以开始排序了,根据我们前面的证明,之后也不可能再沉底0,所以这种情况不会存在;对于前一种情况之外的任何情况,我们手上都一定是有0的,而在正式排序这个过程还没有开始的时候,沉入0对我们没有任何坏处,所以与其沉入有效牌,不如沉入0。

        贪心思路自然得证——类似的,套用“与其……不如”可以证明几乎所有的贪心题目的贪心策略。


伪代码:

        读入数据;

        检测牌库底是否部分有序;

        若有序,检测其余部分是否满足条件S',若满足,开始投入有效牌并计数;

        若牌库底无序或有序但不满足S'

                不断抽牌,沉入0并计数,直到满足条件S;//注意之前可能是未满足S',现在不论之前情况如何,都是满足S,和S'已经没有关系了

                满足S后,开始投入有效牌并计数;

         输出;


贴代码:

        

    #include <bits/stdc++.h> 
    using namespace std;  
    typedef long long ll;  
    const int INF = 0x3f3f3f;  
      
      
      
    int main(){  
        //ifstream infile("input.txt", ios::in);  
        //ofstream outfile("output.txt", ios::out);  
      
        int n;  
        cin >> n;  
      
        vector<int> deck;  
      
        int temp;  
        for(int i = 0; i < n; i++){  
            cin >> temp;  
        }  
        for(int i = 0; i < n; i++){  
            cin >> temp;  
            deck.push_back(temp);  
        }  
      
        int pt;  
        pt = deck.size() - 1;  
      
        int last = deck[pt];  
        int tmp = last;  
        
        //以下判断是否部分已序
        bool isSorted = false;  
        if(last == 1){  
            isSorted = true;  
        }  
        else while(pt--){  
            if(deck[pt] == tmp - 1){  
                if(deck[pt] == 1)  
                    isSorted = true;  
                tmp = tmp - 1;  
            } else{  
                break;  
            }  
        }  
      
        //部分已序,直接开始投入有效牌
        if(isSorted){  
            for(int i = 0; i < deck.size() - last; i++){  
                if(deck[i] == 0)  
                    continue;  
                if(i + 2 + last - deck[i] > 0){  
                    isSorted = false;  
                    break;  
                }  
            }  
        }  
      
        if(isSorted){  
            cout << n - last << endl;  
            return 0;  
        }  
      
        
        //一般情况,从头开始
        int maxDif = 0; //最大偏移量,即总的操作次数 
        for(int i = 0; i < deck.size(); i++){  
            if(deck[i] == 0)  
                continue;  
            else if(i + 2 - deck[i] > 0 and i + 2 - deck[i] > maxDif)  
                maxDif = i + 2 - deck[i];  
        }  
      
        cout << maxDif + n << endl;  
      
      
      
        return 0;  
    }  

        

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里之码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值