写在前面:
本系列博客仅作为本人十一假期过于无聊的产物,对小学期的程序设计作业进行一个总结式的回顾,如果将来有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个数,表示小张手中的牌。
第三行n个数,表示牌堆,数组从左向右的顺序表示牌堆自顶向下的顺序。
Output
一个整数,表示最少执行的操作数。
测试用例 1 | 以文本方式显示
| 以文本方式显示
| 1秒 | 64M | 0 |
测试用例 2 | 以文本方式显示
| 以文本方式显示
| 1秒 | 64M | 0 |
题意分析:
很有意思但不难的一道题。
首先,总共2n的牌里有n张是0,而0是不参与最后的排序的,这说明什么呢?说明我们可以不需要付出交还手上已有的1~n这些牌(称为有效牌)为代价,就可以从牌库摸牌(因为手里有n张牌,若牌库里仍然有我们想要的牌,则我们手里必定会有0,这样我们只需要把0沉底,再从牌库顶摸牌就行)。这种“白嫖”的做法大大简化了我们的解题思路,这也正是这一题“贪心”的来源。我们就可以把整个操作流程分为两个阶段:
①若干次地将手中的0沉底,同时摸一张牌;
②在某个合适的时机,开始把手里的有效牌依次沉底,直到达到目标。
可以证明,任意其他的策略都不如以上这个策略有效,这一定是最优策略(证明方法读者可以自行思考,不算太难,我放在这一部分的下方)。说是两个阶段,实际上的关键就是一个时间点,即——什么时候能开始将有效牌沉底。我们先思考较为普通的情况,即连续抽牌若干次,直到牌库满足了某个条件S,我们就开始沉底有效牌。那么这个条件S是什么呢?首先,1必须在手里,因为我们之前连续抽了若干次牌,牌库底必定是0,此时如果1不在手里,就无法开始排序;其次,2要么在手里,要么在牌库顶,因为我们在放完1之后马上就要放2,在这个过程中我们只有一次的抽牌机会,所以我们需要在一次之内让2进入手牌,所以2要么现在就在手里,要么下一抽正好进入手里……类似的,对于有效牌,它都必须满足,要么在手里,要么在牌库从上数起的
张内,这就是我们想达到的“某种条件S”。那么要抽几次才能达到这种条件?我们就要看,离达到这种条件“最远”的那张牌“究竟有多远”,举个例子,
时,如果牌库自顶向下依次是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,这样我们就用
次完成了全部操作,根据我们证明出来的贪心策略,这就是最优解。
然而以上我们讨论的只是最一般的情况,有没有特殊情况呢?当然有,最最特殊的情况就是——牌库自顶向下已经帮我们排好1~n了,那我们啥也不用做就得到了最终结果;差一点的情况是已经帮我们排好了1~n-1,我们只要美滋滋地扔进去一张n,就大功告成; 最让人烦恼的情况是,从第二张到牌库底依次为2~n,而1还孤零零地在手上,或者已经帮你排好了1~k-1,但k不在手上,此时就算他已经帮你完成了99%也无济于事,依然要全部推翻、从头再来。总结一下,什么情况下我们可以半路插队,直接进入操作②呢?应该要满足两个条件,其一是牌库自底向上依次是k~1,其中,其二是当前的手牌和牌库能够满足条件
,使得在连续投入有效牌的过程中不会因为没牌而间断。条件二的判定也很简单,与之前完全类似,如果牌库底已经从1排到了k,那么这时候
必须在手中,
则必须在牌库顶向下的前
张内。这时候就可以放心大胆地投入有效牌啦。
贪心策略的证明:
用反证法,先证②是必要的,那么我们就假设我们偏不按②来,即在投入若干张有效牌的过程中间沉底一张“0”——这时候发牌姬都恨不得骂你一句“XX”。因为你把已经完成的牌库底的部分序列成功破坏了,得,一切从头再来,很显然不行。因此一旦开始投入有效牌,就不可能沉底0。
再来证明①,如果我们偏不按①来,在沉若干张0的过程中沉入有效牌会怎样?分两种情况,如果这时候你的所有有效牌1~n都已经到手上,那么这时候就已经可以开始排序了,根据我们前面的证明,之后也不可能再沉底0,所以这种情况不会存在;对于前一种情况之外的任何情况,我们手上都一定是有0的,而在正式排序这个过程还没有开始的时候,沉入0对我们没有任何坏处,所以与其沉入有效牌,不如沉入0。
贪心思路自然得证——类似的,套用“与其……不如”可以证明几乎所有的贪心题目的贪心策略。
伪代码:
读入数据;
检测牌库底是否部分有序;
若有序,检测其余部分是否满足条件,若满足,开始投入有效牌并计数;
若牌库底无序或有序但不满足:
不断抽牌,沉入0并计数,直到满足条件;//注意之前可能是未满足
,现在不论之前情况如何,都是满足
,和
已经没有关系了
满足后,开始投入有效牌并计数;
输出;
贴代码:
#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;
}