目录
【题目】
P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
一、题目分析
题目说“两个大脑可以同时计算 2道不同的题目,但是仅限于同一科”、“必须一科一科的复习”,因此每个科目花费的时间是相互不影响的。也就是其实这里看似有4个科目,其实每个科目依次求出最短复习时间,最后每个科目花费的最短时间相加即为答案。
因此我们主要考虑的是如何求单个科目的最短复习时间。以下讨论仅考虑一个科目。
已知“两个大脑可以同时计算 2道不同的题目,但是仅限于同一科”。那么问题就是决策左右脑分别要复习哪些题目。那么如果左右脑能尽量同时工作,也就是如果左右脑复习时间都接近一半,那花费的时间就是最短的。
二、暴力遍历
(一)二进制遍历
一道题不是分配给左脑复习,就是分配给右脑复习。也就是每道题只有两种情况,使用二进制遍历。假如需要复习n道题,那一共有2^n种情况。遍历范围为[0,2^n-1]。
(以下内容针对不清楚什么是二进制遍历的朋友)
一道题要么分配给左脑,要么分配给右脑。假如分配给左脑记为0,分配给右脑记为1;假如要复习的3道题分别是A,B,C。那么这四道题对应的2^3也就是8种情况分别是:
| A | B | C | |
| 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 2 | 0 | 1 | 0 |
| 3 | 0 | 1 | 1 |
| 4 | 1 | 0 | 0 |
| 5 | 1 | 0 | 1 |
| 6 | 1 | 1 | 0 |
| 7 | 1 | 1 | 1 |
可见0~2^3-1对应的二进制数可以正好表示这些题目的分配情况。也就是说,遍历0~7就可以遍历每一种情况。再将这个数字看成二进制数,分解出每一位的数值,对应到每一道题的分配情况即可。
如在(0~7)中任取一种情况,情况6,对应二进制数就是110,那对应就是A(右脑)、B(右脑)、C(左脑)。然后再分别算出左脑的复习总时长为C的时长(Tc),右脑需要复习的时长为A的时长+B的时长(Ta+Tb)。那么情况6分配的情况所花的时间为max{Tc,Ta+Tb}
二进制遍历求解代码
#include<iostream>
#include<cmath>//包含pow()函数
using namespace std;
//二进制遍历:此函数求解单个科目的最短复习时间。输入n为该科目需要复习的题数
int fun2(int n){
if(!n)return 0;
int questions[n],sumT=0;//questions:每道题目需要花的时间 sumT:所有题目总时长
for(int i=0;i<n;i++){
cin>>questions[i];
sumT+=questions[i];
}
//以下开始二进制遍历
int minTime=sumT;
for(int i=0;i<pow(2,n);i++){ //外层循环遍历每一种情况
int tmp=i,left=0,right=0; //tmp:当前情况 left:左脑用时 right:右脑用时
for(int j=0;j<n;j++){ //内层循环将当前情况tmp分解为每一道题的分配情况并处理
if(tmp%2==0)left+=questions[j];
else right+=questions[j];
tmp/=2;
}
if((left>right?left:right)<=minTime)minTime=(left>right?left:right);
}
return minTime;
}
int main(){
int a,b,c,d;
cin>>a>>b>>c>>d;
cout<<fun2(a)+fun2(b)+fun2(c)+fun2(d)<<endl;
return 0;
}
(二)深度优先搜索遍历(递归)
遍历每一种情况还有一种方法就是深度搜索(用递归实现)。我将用一个例子解释该算法。所谓搜索的就是下面这个树状图。以下图显示了所有题的分配情况。 图片解释如下:
假设有3道题分别要分配,分别是A,B,C。它们的耗时分别为T(A)=1,T(B)=2,T(C)=3。T(左脑)表示该情况下左脑的耗时,T(右脑)表示该情况下右脑的耗时。T_sum表示某一种具体分配情况左右脑的总耗时(由于左右脑可以同时工作,我们知道T_sum=max(T(左脑),T(右脑) ) )。

从“开始”结点开始,往下每个结点指向两个结点,意思是下一题分配给左脑或者右脑,从“开始”根结点到最后的叶节点,每一条路径代表一种分配情况。从“开始”结点走到叶结点即可知道这条路径对应的题目的分配情况,就可以知道该情况下左右脑总耗时,进而比较每个路径的总耗时即可知道最优解。
如路径“开始——左脑——右脑——右脑”表示题A分配给左脑,题B、C分配给右脑。因此T(左脑)=T(A)=1,T(右脑)=T(B)+T(C)=5。该路径(该情况)下总耗时为max(1,5)=5。同理可以算出其他情况总耗时,可以知道最小耗时为3。因此最优解为3。
深搜递归遍历代码
int fun5_search(int left_sum,int right_sum,int n,int layer,int questions[]){
if(layer==n) return left_sum>right_sum?left_sum:right_sum;
return min(fun5_search(left_sum+questions[layer],right_sum,n,layer+1,questions),
fun5_search(left_sum,right_sum+questions[layer],n,layer+1,questions));
}
int fun5(int n){
if(!n)return 0;
int questions[n],sumT=0; //questions:每道题耗时 sumT:总耗时
for(int i=0;i<n;i++){
cin>>questions[i];
sumT+=questions[i];
}
return fun5_search(0,0,n,0,questions);
}
其中递归函数为fun5_search()。下面我详细解释该函数:
- 参数 left_sum:当前结点表示路径中,左脑总时长。如“开始”结点,还没开始给左脑/右脑分配题目,所以left_sum=0。如第三行第二个结点,它表示路径“开始——左脑——右脑”,左脑仅被分配了题A,因此left_sum=1。
- 参数 right_sum:当前结点表示路径中,右脑总时长。
- 参数 n:题目总数
- 参数 layer:当前遍历到的层数。根节点是0层,处理题A的结点是1层……
- 参数 questions:存储了每一道题耗时的数组
递归函数fun5_search()首先进行边界判断。如果递归到了边界(layer==n),就返回当前结点T_sum(也就是max(T(左脑),T(右脑) ) )。否则,调用fun5_search()遍历该结点下的两个子结点,使其继续往下搜索。
如搜索当前结点的左结点:
fun5_search(left_sum+questions[layer],right_sum,n,layer+1,questions)
(当前题目分配给左脑,往下传递的left_sum要加上当前题目的耗时 questions[layer] )
三、贪心算法(WrongAnswer)
开篇唠唠:贴主看到这一道题的第一反应其实是贪心算法,然后发现WA了……QAQ。我提交代码的时候简直自信满满,我觉得我想的这个算法简直无懈可击!怎么会错呢?然后一看洛谷的题解发现好多人的第一想法都是贪心。但是没有人证明贪心为什么是错的……贴主本想看看测试用例看看贪心究竟错在哪里,但是洛谷貌似没有开放这道题的测试用例。OK,fine。于是贴主还是想出了一个反例,用来说明贪心为什么不适用。
回顾这个问题,这个问题主要是给左右脑分配情况,使得左右脑复习题目花的时间尽可能均衡。
那我先将题目所花的时间降序排好,依次配分题目给左右脑。记录左右脑目前需要处理题目的总时长为left、right。如果left>right,就将剩下未分配题目中,时间最大的分为右脑;否则,分配给左脑。代码如下:
//这是WA代码!!!!!
//这是WA代码!!!!!
//这是WA代码!!!!!
int fun(int n){
int v[n];
for(int i=0;i<n;i++)cin>>v[i];
//贪心
sort(v,v+n);//对所有题目按照需要的时长升序排序
int left=0,right=0;
for(int i=n-1;i>=0;i--){//从时长最大的题目开始分配
if(left<=right){
left+=v[i];
}
else right+=v[i];
}
return right>left?right:left;
}
错误样例(针对函数):需要复习5道题,每道题时长分别为4,5,3,3,3;
此贪心算法的划分将是 左脑:5,3 ;右脑:4,3,3
但是答案最优的分配应该是 4,5;3,3,3,这样两边脑子正好每个脑子复习时间都为9。
四、动态规划(01背包问题)
这道题数据规模小,暴力求解也能跑得动。但暴力求解的时间复杂度,根据数据规模呈指数级增长!如果这道题需要求解的题目不止20道,而是200道呢?循环的次数将会达到2^200次(约为10的60次方)……
这里我们还有一种方法:动态规划。我们可以把这道题抽象为01背包问题。01背包是动态规划的经典问题,在此不做赘述。我主要讲讲为什么这道题如何抽象为01背包问题。
01背包问题概述
你有体积为V的背包,和n件物品。每件物品的价值分别为Pi,Vi。每件物品只能选择装进背包或者不装进背包。问要选择带哪些物品才能使背包内物品价值最大?
我们知道01背包问题的解决核心在于维护一张表:
| P(i,v) | 0 | 1 | 2 | 3 | … | V-2 | V-1 | V |
| i=0 | 0 | 0 | 0 | 0 | … | 0 | 0 | 0 |
| 1 | 0 | |||||||
| … | 0 | |||||||
| n | 0 |
其中第一行是背包总体积,第一列是商品序号。表格内P[i,v]代表背包为v时,只考虑前i件物品时,背包能取到最大价值为P[i,v],也就是表格中每一格记录的内容。
状态转移方程:P[i,v]= max{ P[i-1,v-Vi] + Pi , P[i-1,v] }
稍微解释一下这个方程的含义:当背包大小为v,只考虑前i件物品的时候,我要么取第i件物品,要么不取第i件物品。如果我取第i件物品,那么背包总价值为第i件物品的价值Pi,加上背包体积为v-Vi(当前体积v减去第i件物品的体积)时,只考虑前i-1件物品时的背包最大价值。如果不取第i件物品,那背包的总价值应该和当前体积下,只考虑到前i-1件物品时的总价值一样。这两种情况取最大值
这张表将被初始化成上表的模样,然后利用状态转移方程从左往右依次遍历每一行,即可得到整张填充整张表。最终01背包问题的解就是表中的最后一格数据P[n,V]。
抽象为01背包问题
这个问题主要是让左右脑复习时长尽可能相等,也就是希望某一边脑子,如左脑的复习时长尽可能为总时长T(所有题目复习所需时间之和)的一半 1/2T。当一边脑子最接近1/2T时,另一边脑子自然达到了最优解。
假设左脑的复习时长T(left)≤1/2T,那么左脑就相当于一个总体积为1/2T的背包,它需要装入“价值”尽可能多的题目,使得左脑“价值”最大。唯一不同的一点是,此处“价格”为选择题目的时长。当我们的左脑求得最优解的时候,右脑的时长自然也为最优。
那么我们就得到需要维护的表格如下:
| P(i,t) | 0 | 1 | 2 | 3 | … | 1/2T-2 | 1/2T-1 | 1/2T |
| i=0 | 0 | 0 | 0 | 0 | … | 0 | 0 | 0 |
| 1 | 0 | |||||||
| … | 0 | |||||||
| n | 0 |
其中第一行是当前左脑最大复习时长(背包总体积),第一列是题目序号(商品序号)。表格内P[i,t]代表左脑最大复习时长为t时,只考虑前i道题时,左脑能复习的最大时长为t],也就是表格中每一格记录的内容。
状态转移方程:P[i,t]= max{ P[i-1,t-Ti] + Ti , P[i-1,t] }
动态规划代码
int fun3(int n){
if(!n)return 0;
int v[n+1],sumt=0;
v[0]=0;
for(int i=1;i<=n;i++){
cin>>v[i];
sumt+=v[i];
}
//以下是动态规划
int sumT=sumt;
sumt=(sumt+1)/2;//求得左脑最大复习时间
int bag[n+1][sumt+1];
for(int i=0;i<=n;i++) //初始化表格
for(int j=0;j<=sumt;j++) //初始化表格
bag[i][j]=0; //初始化表格
for(int i=1;i<=n;i++){ //遍历每一道题,决策是否给左脑复习;i是某一道题
for(int j=1;j<=sumt;j++){ //j是当前左脑可以用的复习总时长
if((j>=v[i])&& v[i]+bag[i-1][j-v[i]]>bag[i-1][j]) //状态转移方程式
bag[i][j]=v[i]+bag[i-1][j-v[i]]; //状态转移方程式
else bag[i][j]=bag[i-1][j]; //状态转移方程式
}
}
return bag[n][sumt]>(sumT-bag[n][sumt])?bag[n][sumt]:(sumT-bag[n][sumt]);
}
如果要求高的话,这道题还可以优化一下空间利用。观察状态转移方程式我们可以发现每一行的值只和上一行的值有关。因此其实这个二维的表格只保存一行即可。只是需要注意在进行动态规划迭代的时候,第二层循环应该从最后一位开始。即每一行按照从右往左的顺序迭代。如果仍然从左往右遍历的话,可能会导致前面的值更新了变成P[i,v],而后面的值需要用到P[i-1,v],却误用为P[i,v]的情况。
动态规划-空间优化后代码
int fun(int n){
if(!n)return 0;
int v[n],sumt=0;
for(int i=0;i<n;i++){
cin>>v[i];
sumt+=v[i];
}
//以下是动态规划
int sumT=sumt;
sumt=(sumt+1)/2;
int bag[sumt+1]; //一维表格保存一行即可
for(int i=0;i<=sumt;i++)bag[i]=0; //初始化状态转移表
for(int i=0;i<n;i++){
for(int j=sumt;j>=0;j--){ //注意右往左遍历!!
if(j<v[i])continue;
if(v[i]+bag[j-v[i]]>bag[j]){ //状态转移方程式
bag[j]=v[i]+bag[j-v[i]]; //状态转移方程式
}
}
}
return bag[sumt]>(sumT-bag[sumt])?bag[sumt]:(sumT-bag[sumt]);
}

2769

被折叠的 条评论
为什么被折叠?



