洛谷P1120 小木棍(升级版)

本文深入探讨了一道经典的搜索剪枝题目,通过详细的步骤分析和优化策略,讲解了如何利用深度优先搜索(DFS)算法解决木棍拼接问题。文章涵盖了从输入过滤、枚举策略、搜索算法设计到多种优化技巧,包括木棍排序、剪枝条件、二分查找等,最终实现了高效的求解过程。
传送门啦

一道经典的搜索剪枝题,不废话,步入正题。

分析:

一、输入时手动过滤不合法的情况

二、很明显我们要枚举把哪些棍子拼接成原来的长棍,而原始长度(原来的长棍的长度)都相等,因此我们可以在 $ dfs $ 外围枚举拼接后的每根长棍的长度。那枚举什么范围呢?
  其长度至少是最长的一根木棍,此时最长的这根木棍恰好单独组成原来的长棍。否则这根最长的木棍就无法自己或与其它木棍组成原来的长棍。其长度至多是所有木棍的长度之和,此时所有的木棍拼在一起恰好成为一根原来的长棍。
   $ dfs $ 一定会超时。所以我们考虑到当原始长度 不能被 所有木棍的长度之和 整除的话,这些木棍是拼不出整数根的(如果都拼成枚举的原来长棍的长度)。
  原始长度枚举到 所有木棍的长度之和/2 即可,因为此时所有木棍有可能拼成2根木棍,原始长度再大的话就只能是所有木棍拼成1根了,所以如果最后我们没有得出一个合法的方案,就直接把 $ sum $ 输出,即原来木棍是一根长度为 $ sum $ 的木棍。

三、下面就该考虑怎么搜索了。

dfs(int k , int last , int rest)分别表示正在拼第几根原来的长棍,使用的上一根木棍的编号,当前在拼的长棍还有多少长度未拼

光是这些肯定是不够的。。

四、各种优化。

1.一根长木棍肯定比几根短木棍拼成同样长度的用处小,可以说是短木棍灵活(??),所以对输入的所有木棍按长度从大到小排序,这样短木棍可以更加灵活地接在原始木棍上。

2.根据1,将输入的木棍排好序后,当用木棍 $ i $ 拼合原始长棍时,从第 $ i + 1 $ 根木棍开始往后搜。在分析二中我们也讨论了原始木棍的范围了,大于等于最长的一根木棍。

3.当 $ dfs $ 返回拼接失败,需要更换当前使用的木棍时,不要再用与当前木棍的长度相同的木棍,因为当前木棍用了不行。可以用 $ next[ ] $ 预处理出了排序后每根木棍后面的最后一根与这根木棍长度相等的木棍,它的下一根木棍就是第一根长度不相等的木棍了。

4.只找木棍长度不大于未拼长度rest的所有木棍。可以根据木棍长度的单调性来二分找出第一个木棍长度不大于未拼长度 $ rest $ 。它后面的木棍一定都满足这个条件。

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

最后一个优化,也是最难最不好想最不好理解的一个。
其实如果不加最后这一个优化可以得到78分,考场上也是一个比较可观的分数。

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

 当前长棍剩余的未拼长度等于当前木棍的长度时,当前木棍明显只能自组一根长棍,但继续拼下去却失败,说明这根木棍不能自组?!这根木棍不自组就没法用上了,所以不用搜更短的木棍了,直接回溯,改之前的木棍;

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

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

inline int read(){
    //快速读入 
    char ch = getchar();
    int f = 1 , x = 0;
    while(ch > '9' || ch < '0'){if(ch == '-')f = -1;ch = getchar();}
    while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + ch - '0';ch = getchar();}
    return x * f;
}

int n,x,a[70],tot;
int sum,m,len,next[70];
bool ok,used[70];

bool cmp(int a,int b){return a > b;}

void dfs(int k,int last,int rest){
    int i;
    if(!rest){
        if(k == m){
            ok = 1;
            return ;
        }
        for(i=1;i<=tot;i++)
            if(!used[i])  break;
        used[i] = 1;
        dfs(k + 1 , i , len - a[i]);
        used[i] = 0;
        if(ok)  return;
    }
    int l = last + 1 , r = tot , mid;
    while(l < r){
        //二分查找,优化4 
        mid = (l + r) >> 1;
        if(a[mid] <= rest)  r = mid;
        else l = mid + 1;
    }
    for(i=l;i<=tot;i++)
    if(!used[i]){
        used[i] = 1;
        dfs(k , i , rest - a[i]);
        used[i] = 0;
        if(ok)  return ; //找到答案直接退出 
        if(rest == a[i] || rest == len)  return ;
        //优化6
        i = next[i];
        if(i == tot)  return;
    }
}

int main(){
    n = read();
    for(int i=1;i<=n;i++){
        x = read();
        if(x <= 50){
            a[++tot] = x;
            sum += x;
        }
    }
    sort(a + 1 , a + 1 + tot , cmp);//优化1 
    next[tot] = tot;
    for(int i=tot-1;i>0;i--){
        //优化 3 
        if(a[i] == a[i+1])  next[i] = next[i+1];
        else next[i] = i;
    }
    for(len=a[1];len<=sum/2;len++){ //从最长的开始搜,优化2
        //枚举到sum / 2,分析二 
        if(sum % len != 0)  continue; //无法整除直接过滤,分析二 
        m = sum / len;
        ok = 0;
        used[1] = 1;
        dfs(1 , 1 , len - a[1]);
        used[1] = 0;
        if(ok){
            printf("%d\n",len);
            return 0;
        }
    }
    printf("%d\n",sum);
    return 0;
}

转载于:https://www.cnblogs.com/Stephen-F/p/9871029.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值