codeforces732D 二分加贪心。。
啊,不好意思。。这个题窝也不会,看了网上的题解做的。。
先说一下题意。。
你要考试了。。然后你有n天的时间准备以及进行考试
并且,每一天。。要么你花一天时间去考一门(每天只能考一门),要么花一天时间休息,要么花一天时间复习。。
然后给你一个考试安排的序列(值为考试科目编号,值为零说明今天不能考试),给你一个每门考试至少要复习的时长的序列。。
然后问你最短多长时间你能顺利考完试。。不能考完就输出-1
题意:
给出一串数字,代表了第i天能够进行哪场考试,如果为0就不能考试,每天只能考一场。一共要考m场试,一个序列a[i],每门科目要a[i]的复习时间,所以问最快第几天能够考完所有科目。
题目连接 http://codeforces.com/problemset/problem/732/D
首先我们看到这个题。。md。。有种首尾不能相顾的感觉。。
但是呢,这个题很明显是让我们去找一个符合条件的,最小的值,这不就是二分的定义么。。
而且这个题很明显地有一个性质。。那就是时间越长你就越容易通过所有考试,时间越短当然也就越难通过
这是一个单调的性质。。
但是一开始并没有看出来。。。
比如说001230102,1和2都只需要1天时间复习,科目3需要4天时间复习
如果我们采用先安排最早考试的场次先考。。那么我们肯定是先去考1和2这两场了。。
然后我们就发现了。。如果你这么搞。。科目3铁定爆炸。。,我们其实可以选择靠后的1,2两场
如果你选了靠后的1,2如果后面紧接一个4,那还是爆炸了。。说不定把前面全排空了才来得及复习4,
而且你不确定排空到哪里。。我感觉应该是依赖于后续状态的。。
现在这么考虑的话。。应该有两个难点。。第一不知道后面的情况。。现在的决策就不一定正确。。
第二我们不知道应该按照何种顺序去考虑,或者是进行dp
//第一解决不知道后面的情况。。我们可以用dfs
我先在的想法是每个有编号的节点之间都连一条边。。然后中间所隔0的个数就可以当成这条边的权值
然后按照这条链去dfs一下?
//第二解决何种顺序。。我们强行定义一个顺序解决。。
dfs:我们按照考试的顺序去判断。。强行定一个1~m的顺序。。如果这1~m门课都考完了,这事不就办成了么。。
但是其实这样的话。。如果我们从科目k的若干考试时间选一个, 然后从这个时间往前,我们需要a[k]的考试
复习时间。。如果你这个时间往前未被使用的时间大于a[k](这里埋了一个坑,判断哪些时间未被使用)
我们就需要作出决定,我们要用这些未用时间的哪些天数来凑出一个a[k]的复习时间呢,
因为我们知道。。你选择不同的那些天。。可能会把一些本来考试的时间用来复习,并且是用来复习你这一科
那你后面搜索的状态集合就肯定会受你这个得影响。。但是没关系。。我们要跑遍所有集合。。
如果我们要进行如此的状态转移。。那我们还要维护一个vis[maxn]的状态。。
然后如果我们想从终态往上推的话。。首先终态是上完这m门课。。时间是m门中考试最晚的一门的考试时间
那么我们还要构造终态的vis[maxn]数组。。你如果按照科目编号状态转移。。中间很可能会空很多没有用的
天数。。对于这种情况我们需要去构造一下?但是这样枚举的话不就又成了dfs了?而且假如你能很快地构造出
dfs的叶子节点。。那么我们如何去递推他的父亲呢。。
那么我们要找n-1门课。。他的下层叶子节点假设全tm搞出来了。。那我也不知道怎么搞一个策略(可能是贪心的)
确定出来我要找哪个叶子节点,找到了这个叶子节点之后如何递推到他的父亲节点。。(枚举他的父亲,当然知道它的父亲是谁)
然后我们发现知道了叶子节点,如果他有多个可选择的n-1...我们还是不能确定地选出哪个n-1..虽然知道用了。。但是不知道是用来
复习了还是用来考试了。。考试就不能瞎调度了。。
想到这里我觉得可以分两个vis,vis1标记考试,vis2标记复习,但是你这样搞不就不用回溯了么。。也就是说只能是dfs从顶向下(其实顺序根本无所谓)跑出来的
直接去标记最晚最小者是答案。。大暴力啊。。能跑完么。。
所以说。。这么定义状态不行。。毛线的关系都没有。。放弃乱搞吧还是。。(hahahaha
===
其实这个题首要解决的问题有俩。。第一给你一个时间n你怎么去判它是合法的。。第二在判断合法的情况下,我们如何选择最小的
主要是第一个问题怎么解决。。
我们来分析第一个样例
7 2
0 1 0 2 1 0 2
1 2
首先我们知道这个答案是5,最短5天
5是一个合法的答案,如果我们按照先满足考试早的顺序来,先满足1,那2就肯定满足不了了。。然后5就不合法了
那么我们按照考试最晚的顺序来。。先满足后面那个1,那你要从后面四天选一天,显然第四天不能选,选了就gg,因为只要有一门没考完,这个事就没办好
然后我们可以选第三天,然后第4天的考试我们可以选。。因为第三天用过了,我们选1,2两天复习
然后我们也可以选第二天,然后第4天的考试我们可以选1,3两天复习
然后我们也可以选第一天,然后第4天的考试我们可以选择2,3两天复习
8 3
1 2 3 0 2 0 1 2
1 1 2
我先在的想法是贪心地从后往前取,然后贪心地先取离自己最近的零
假如上面的这个例子里面,我们后面的1,2如果不取离自己最近的零,那我们就可以去
前面的零或者是和自己编号一样的来复习,因为你现在决定考这个科目较晚的一场,那么我们就可以把较早的那场用来复习,或者看成是0也没有关系
所以可以取前面的1,2但是这样的话3就gg了。。既然我们可以取前面的1,2那么他们就可以看成是0,既然是0,那么我们完全可以交换0的位置,交换完全
不受影响,所以我们把前面这两个0和后面那两个没用过的零换一下,这样3前面有两个0,条件也满足了。。
10 3
0 2 0 1 0 0 1 2 3 2
1 1 4
从第10位判合法性。。先不要着急判最短,判合法即可
先满足最后一个2,后面的0和2就都变成0,随便取一个
假设取第五位的0,然后我们考虑3,从前面的0取4个,乍看不够是吧,因为5用过了
但是别忘了前面的2都变成0了,然后3取2,3,6,8的0,然后我们再考虑1 。。
1可以取第一位的0 。。然后就满足了
10 3
0 2 0 1 0 0 1 2 3 2
1 3 4
首先考虑最后一个2,考虑5,6,8三个0
然后对于3,考虑1,2,3三个0,woc零不够了。。
那说明没得交换了。。然后就gg
10 3
0 2 0 1 0 0 1 2 3 2
1 2 4
对于最后一个2,考虑6,8两个0
然后对于3取,1,2,3,5这四个零
然后对于1,后面的那个1就变成0了,取上
然后条件就满足了
11 3
0 0 2 0 1 0 0 1 2 3 2
1 2 4
根据前面的分析。。现在应该很熟练了吧 = =
所以说一旦我们每次所需的在自己前面位置的零的个数足够,如果是
因为0的位置取得不一样而导致某个考试不能完成,即复习内容不对
那因为都是0嘛,所以我们可以xjb交换满足条件。。
所以我们看了若干组数据才明白。。我们一旦把一些数据都搞成零以后,那么这些零就可以随便交换
我们就不要担心,到底要取哪些天用来复习,我们不用关心取这些零的哪些位置,而是只考虑个数就可以
所以说我们只考虑维护当前可用个数就能判断出来这组数据合不合法
(我这个其实不算是贪心证明,但是这是一种思想)
(多看数据,多试方法,多发现基本事实)
其实还是有点不清楚。。毕竟贪心很难得嘛。。或者我们可以这么想,当我们固定了时间之后,我们只需要判这个数据最宽松的做法
合不合法就行,你又不用判最短的。。这样你就把最短这一维去掉了。。所以说考试时间越晚我们越容易准备对吧。。(直觉上)
所以说我们先满足这个宽松的条件,然后只要满足就好啦,找到一组可行解能判断你合法就好啦,我管你条件是宽松还是紧张,这个其实
有点类似于放缩的思想。。(最优解不好找我们先想办法找最好满足的可行解)
而且这个东西关键在于从后往前。。因为这个时间确定了之后就可以从后往前
从前往后,一方面你过早地做了决定不好,你不知道后面的情况。。然后你往后走了一个位置
这个位置当然也是依赖后面的位置。。那就一直往后咯,我们需要找一个确定态。。那么好了
我直接找最后不就完事了么。。,然后我们从后往前考虑不就好了么。。
这个一条链的。。我们就别递归了吧。。浪费时间。。直接从后往前回溯得了
咳。。费了老大劲了才想出一个如何判合法的方法。。
这个思维的惯性不好。。,往后依赖了一步就懵逼了,继续往后依赖啊,只要到结尾不就好了么。。
然后我们从结尾往前就好了啊。。
找出判合法之后,然后我们发现答案有二分的性质,然后我们就二分答案,根据判合法函数找最小的满足条件的答案
下面上代码吧。。
其实这个题的贪心策略是:首先我们希望每门课的考试时间越晚越好,然后就是先复习最早开始的考试。
我们后面再分析这个贪心的策略
#include <iostream> #include <cstdio> #include <string.h> #include <algorithm> int n,m; const int maxn=1e5+7; int d[maxn],a[maxn]; bool vis[maxn]; bool yes(int n){ memset(vis,0,sizeof(vis)); int cur=n; register int i; for(i=n;i>=1;--i){ //--写成了++ if(cur>i-1) cur=i-1; if(d[i]&&!vis[d[i]]&&a[d[i]]<=cur){ //把d[i]!=0漏了。。 //前面算的时候肯定把当前这天当成资源天数了 //所以现在算的时候,今天用来考试 //要减去一天 cur--; vis[d[i]]=true; cur-=a[d[i]]; } } for(i=1;i<=m;++i){ if(!vis[i]) return false; //把i写成m了。。 } return true; } int main(){ scanf("%d%d",&n,&m); register int i; for(i=1;i<=n;++i){ scanf("%d",d+i); } for(i=1;i<=m;++i){ scanf("%d",a+i); } int left=1,right=n,mid; while(left<=right){ mid=(left+right)>>1; // printf("mid:%d yes:%d\n",mid,yes(mid)); if(yes(mid)) right=mid-1; else left=mid+1; } //left right写反了。。 int ans; if(left>n){ //no anwser printf("-1\n"); } else{ if(!yes(right)){ right++;//ans is the next } printf("%d\n",right); } return 0; }