洛谷【P2392】 kkksc03考前临时抱佛脚

目录

【题目】

一、题目分析

二、暴力遍历

 (一)二进制遍历

二进制遍历求解代码

(二)深度优先搜索遍历(递归)

        深搜递归遍历代码

三、贪心算法(WrongAnswer)

四、动态规划(01背包问题)

        01背包问题概述

        抽象为01背包问题

动态规划代码

动态规划-空间优化后代码


【题目】

P2392 kkksc03考前临时抱佛脚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

一、题目分析

        题目说“两个大脑可以同时计算 2道不同的题目,但是仅限于同一科”、“必须一科一科的复习”,因此每个科目花费的时间是相互不影响的。也就是其实这里看似有4个科目,其实每个科目依次求出最短复习时间,最后每个科目花费的最短时间相加即为答案。

        因此我们主要考虑的是如何求单个科目的最短复习时间。以下讨论仅考虑一个科目。

        已知“两个大脑可以同时计算 2道不同的题目,但是仅限于同一科”。那么问题就是决策左右脑分别要复习哪些题目。那么如果左右脑能尽量同时工作,也就是如果左右脑复习时间都接近一半,那花费的时间就是最短的。

二、暴力遍历

 (一)二进制遍历

        一道题不是分配给左脑复习,就是分配给右脑复习。也就是每道题只有两种情况,使用二进制遍历。假如需要复习n道题,那一共有2^n种情况。遍历范围为[0,2^n-1]。

        (以下内容针对不清楚什么是二进制遍历的朋友)

        一道题要么分配给左脑,要么分配给右脑。假如分配给左脑记为0,分配给右脑记为1;假如要复习的3道题分别是A,B,C。那么这四道题对应的2^3也就是8种情况分别是:

       

ABC
0000
1001
201

0

3011
4100
5101
6110
7111

         可见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)0123V-2V-1V
i=00000000
10
0
n0

        其中第一行是背包总体积,第一列是商品序号。表格内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)01231/2T-21/2T-11/2T
i=00000000
10
0
n0

        其中第一行是当前左脑最大复习时长(背包总体积),第一列是题目序号(商品序号)。表格内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]);
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值