1440:【例题1】数的划分 信息学奥赛一本通 深搜的剪枝技巧

小木棍问题求解

题目描述

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长度都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入

  • 第一行为一个单独的整数 NN,表示砍过以后的小木棍的总数,其中 N≤60N≤60。
  • 第二行为 NN 个用空格隔开的正整数,表示 NN 根小木棍的长度。

输出

  • 仅一行,表示要求的原始木棍的最小可能长度。

输入样例

9
5 2 1 5 2 1 5 2 1

输出样例

6

解题思路

总体思路

我们需要找到能够拼接成原始木棍的最小可能长度。具体步骤如下:

  1. 计算总长度:将所有小木棍的长度相加得到总长度。
  2. 枚举可能的原始木棍长度:从最长单根木棍的长度开始尝试,逐个检查每个可能的长度,直到找到一个可行的解。
  3. 回溯法:使用递归函数来尝试拼接木棍,确保每一段的长度不超过当前尝试的原始木棍长度。

关键点

  1. 总长度必须能被最终的木棍长度整除:如果总长度不能被某个长度整除,则不可能用这个长度来拼接所有的木棍。
  2. 回溯法:通过递归尝试不同的组合方式来检查是否可以用当前尝试的长度拼接成所需的木棍数。
  3. 剪枝优化
    • 跳过已使用的木棍:避免重复使用同一根木棍。
    • 跳过超过剩余长度的木棍:避免选择超出当前需要填充长度的木棍。
    • 跳过相同长度的木棍:避免重复计算相同的组合情况。
    • 第一段失败则放弃当前长度:如果当前是某根原始木棍的第一段且失败,说明该长度无效。
    • 正好填满但失败则放弃:如果当前木棍正好填满剩余长度但后续失败,无需尝试更小木棍。

代码实现

下面是完整的C++代码实现,包含了详细的注释以便理解每一部分的功能。
 

// 包含所有标准库头文件(竞赛常用写法)
#include<bits/stdc++.h>
using namespace std;

// 全局变量声明
int n;                // 小木棍总数量
vector<int> a;        // 存储所有小木棍长度的数组
int sum;              // 所有小木棍的总长度
int max_len;          // 最长小木棍的长度
int len;              // 当前尝试的原始木棍长度
int cnt;              // 原始木棍的总根数(sum / len)
vector<bool> used;    // 标记数组,记录每个小木棍是否被使用

// 深度优先搜索函数
bool dfs(int rest,     // 当前原始木棍剩余需要拼的长度
         int next_idx, // 下一个可选的木棍索引(避免重复组合)
         int parts_left) { // 剩余需要完成的原始木棍数量
    
    // 成功条件:所有原始木棍都已拼完
    if (parts_left == 0) return true;
    
    // 当前原始木棍拼完,开始拼下一根
    if (rest == 0) {
        return dfs(len, 0, parts_left - 1); // 新木棍从索引0重新开始选
    }

    // 遍历可选木棍(从next_idx开始,避免重复组合)
    for (int i = next_idx; i < n; ++i) {
        // 跳过已使用的木棍
        if (used[i]) continue;
        // 跳过超过剩余长度的木棍
        if (a[i] > rest) continue;
        
        /*----- 关键剪枝1:跳过相同长度的木棍 -----*/
        // 前提:数组已排序,且前一个相同长度的木棍未被使用
        if (i > 0 && a[i] == a[i-1] && !used[i-1]) continue;

        // 尝试选择当前木棍
        used[i] = true;
        
        // 递归尝试:用当前木棍填充剩余长度
        if (dfs(rest - a[i], i + 1, parts_left)) {
            return true; // 后续递归成功则直接返回
        }
        
        // 回溯:撤销当前选择
        used[i] = false;

        /*----- 关键剪枝2:第一段失败则放弃当前长度 -----*/
        // 如果当前是某根原始木棍的第一段且失败,说明该长度无效
        if (rest == len) return false;
        
        /*----- 关键剪枝3:正好填满但失败则放弃 -----*/
        // 如果当前木棍正好填满剩余长度但后续失败,无需尝试更小木棍
        if (rest == a[i]) return false;
    }
    
    // 所有可能性尝试失败
    return false;
}

int main() {
    // 读取输入
    cin >> n;
    a.resize(n);
    sum = 0;
    max_len = 0;
    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        sum += a[i];                  // 计算总长度
        max_len = max(max_len, a[i]); // 记录最长木棍
    }
    
    // 优化:从大到小排序(优先使用长木棍,减少递归分支)
    sort(a.begin(), a.end(), greater<int>());

    // 枚举可能的原始木棍长度(从最长木棍到总长度)
    for (len = max_len; len <= sum; ++len) {
        // 剪枝:长度必须是总长度的因数
        if (sum % len != 0) continue;
        
        cnt = sum / len;        // 计算需要的原始木棍数量
        used.assign(n, false);  // 重置使用标记
        
        // 尝试用cnt根len长度的木棍进行拼接
        if (dfs(len, 0, cnt)) { // 初始参数:需要拼len长度,从0开始选,剩余cnt根
            cout << len << endl;
            return 0; // 找到最小长度后立即退出
        }
    }
    
    // 所有可能失败的情况(理论上应该只有sum本身)
    cout << sum << endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值