洛谷1120-小木棍-搜索+剪枝-好题

题解来自洛谷讨论区!

题目描述:

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。

现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。

给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入描述:

共二行。

第一行为一个单独的整数N表示砍过以后的小木棍的总数,其中N≤65

(管理员注:要把超过50的长度自觉过滤掉,坑了很多人了!)

第二行为N个用空个隔开的正整数,表示N根小木棍的长度。

输出描述:

一个数,表示要求的原始木棍的最小可能长度

输入样例:

9
5 2 1 5 2 1 5 2 1

输出样例:

6

核心思想:

前排提示:第四条的优化7讲的是那个不少人不明白的优化,如果你只是不明白那个优化可以空降。

一,管理员已经在题目中告诉你输入时去掉长度大于50的木棍。

二,想好搜索什么。很明显我们要枚举把哪些棍子拼接成原来的长棍,而原始长度(原来的长棍的长度)都相等,因此我们可以在dfs外围枚举拼接后的每根长棍的长度。那枚举什么范围呢?

其长度至少是最长的一根木棍,此时最长的这根木棍恰好单独组成原来的长棍。如果 原始长度 小于 最长的这根木棍,那么这根最长的木棍就无法自己或与其它木棍组成原来的长棍。

其长度至多是所有木棍的长度之和,此时所有的木棍拼在一起恰好成为一根原来的长棍。如果 原始长度 大于所有木棍的长度之和,那么即使所有木棍拼在一起也组不够原来的长棍了。

这么大的循环套dfs会超时么?当然会了。所以我们可以考虑到当 原始长度 不能被 所有木棍的长度之和 整除的话,这些木棍是拼不出整数根的(如果都拼成枚举的原来长棍的长度)。因此在循环时把它们刷掉。

这里借鉴了dalao的(小)优化,即原始长度枚举到 所有木棍的长度之和/2 即可,因为此时所有木棍有可能拼成2根木棍,原始长度再大的话就只能是所有木棍拼成1根了。

三,脑补一下怎么搜。设dfs(int k,int last,int rest),k表示正在拼第几根原来的长棍,last表示使用的上一根木棍(输入的短棍)的编号,rest表示当前在拼的长棍还有多少长度未拼。于是循环枚举下一根将要使用的木棍。

四,上面的做法不超时说明你太强大了。你开始思考对程序做一些优化。(下面的优化请按顺序想)

优化1: 一根长木棍肯定比几根短木棍拼成同样长度的用处小,即短的木棍可以更灵活组合,所以对输入的所有木棍按长度从大到小排序,从长到短地将木棍拼入,这样短木棍可以更加灵活地接在。

如果你还不太清楚“灵活”的含义,请形象脑补一下——如果先用短木棍,那么需要很多根连续的短木棍接上一根长木棍才能拼成一根原来的长棍,那么短木棍都用了后,剩下了大量长木棍,拼起来就不如短木棍灵活,更难接出原始长度。而先用长木棍,最后再用短木棍补刀,这样就剩下了相对较短的木棍,能更加灵活地拼接出原始长度。

优化2:根据优化1,将输入的木棍从大到小排好序后,当用木棍i拼合原始长棍时,从第i+1根木棍开始往后搜。

优化3:当dfs返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行,改成与它相同长度的木棍一样不行。这里我预处理出了排序后每根木棍后面的最后一根与这根木棍长度相等的木棍(程序中的next数组),它的下一根木棍就是第一根长度不相等的木棍了。

这个预处理可以优化时间,不必在循环中慢慢往下找长度不相等的木棍。

优化4:只找木棍长度不大于未拼长度rest的所有木棍。我看其他大部分人的做法(包括书上的啊)都是直接在循环中判断,但我认为可以根据木棍长度的单调性来二分找出第一个木棍长度不大于未拼长度rest。它后面的木棍一定都满足这个条件。

优化5:用vis数组标记每根木棍是否用过。另外在dfs回溯的时候别忘了去掉这些标记,这样就不用每次dfs之前memset了(memset用多的话速度可TM慢了)!

优化5的习惯可以沿用到各种竞赛

优化6:由于是从小到大枚举 原始长度,因此第一次发现的答案就是最小长度。dfs中只要发现所有的木棍都凑成了若干根原长度的长棍(容易发现 凑出长棍的根数=所有木棍的长度之和/原始长度),立刻一层层退出dfs,不用滞留,退到dfs外后直接输出原始长度并结束程序。

优化7:还有一个难想却特别特别重要的优化:如果当前长棍剩余的未拼长度等于当前木棍的长度或原始长度,继续拼下去时却失败了,就直接回溯并改之前拼的木棍。有些人不太明白这个优化,这里简单说一下:

当前长棍剩余的未拼长度等于当前木棍的长度时,这根木棍在最优情况下显然是拼到这(如果用更多短木根拼完剩下的这段,把这根木棍留到后面显然不如把更多总长相等的短木棍扔到后面)。如果在最优情况下继续拼下去失败了,那肯定是之前的木棍用错了,回溯改即可。

当前长棍剩余的未拼长度等于原始长度时,说明这根原来的长棍还一点没拼,现在正在放入一根木棍。很明显,这根木棍还没有跟其它棍子拼接,如果现在拼下去能成功话,它肯定是能用上的,即自组或与其它还没用的木棍拼接。但继续拼下去却失败,说明现在这根木棍不能用上,无法完成拼接,所以回溯改之前的木棍。
题解转自:https://www.luogu.org/problemnew/solution/P1120

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=75;
int a[N],sum,r,d,n,mx;
int nxt[N];
bool vis[N];
bool dfs(int k,int la,int l)
{
    if(k==r+1)
        return 1;
    for(int i=la+1;i<=n;)
    {
        if(vis[i])
        {
            i++;
            continue;
        }
        if(l+a[i]<=d)
        {
            vis[i]=1;
            if(l+a[i]<d&&dfs(k,i,l+a[i]))
                return 1;
            if(l+a[i]==d)
            {
                if(dfs(k+1,0,0))
                    return 1;
                vis[i]=0;
                return 0;
            }
            vis[i]=0;
            if(l==0)
                return 0;
        }
        i=nxt[i];
    }
    return 0;
}
bool cmp(int x,int y)
{
    return x>y;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        if(a[i]>50)
        {
            i--;
            n--;
        }
        else
        {
            mx=max(mx,a[i]);
            sum+=a[i];
        }
    }
    sort(a+1,a+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        //cout<<a[i]<<' ';
        int flag=0;
        for(int j=i+1;j<=n;j++)
            if(a[j]!=a[i])
            {
                flag=1;
                nxt[i]=j;
                break;
            }
        if(!flag)
            nxt[i]=n+1;
    }
    //cout<<endl;
    //for(int i=1;i<=n;i++)
      //      cout<<nxt[i]<<' ';
   // cout<<endl;
    for(r=n;r>0;r--)
    {
        if(sum%r==0)
        {
            d=sum/r;
            if(d<mx)continue;
            if(dfs(1,0,0))
                break;
        }
    }
    cout<<d<<endl;
    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值