【洛谷 P1120】小木棍——搜索剪枝

【洛谷 P1120】小木棍——搜索剪枝

P1120 小木棍 - 洛谷

题目描述

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

数据范围

\(1 \leq n \leq 65\)\(1 \leq a_i \leq 50\)

时空限制

260ms,128MB。

题意理解

给定一个多重数集,将其划分为若干子集,保证存在至少一种划分方式使得各子集中元素之和相等,求上述和的最小值。

算法分析

暴力算法

根据题意和数据范围,考虑迭代原始木棍长度,每次进行 最暴力 的 DFS 直到找到一种可行方案,时间复杂度为 \(O(nm \cdot n!)\)

优化一

显然,当原始木棍比已知最长木棍短时,不可能有解,故直接从已知最长木棍的长度开始迭代。

优化二

显然,当原始木棍长度不能被木棍总长度整除时,不可能有解,直接跳过。

优化三

因为每次搜索的目的是证明解的存在性,所以只要找到一组解就可以停止搜索。

利用暴力算法结合以上三种最简单的优化,可以通过本题 30 组数据中的 9 组。

优化四

注意到在拼凑每一根原始木棍时,如果每次选取都尝试所有未使用的木棍会造成大量重复搜索,故改为只尝试比上一次选取更靠后的木棍。

优化五

注意到已知木棍中出现较多长度相等者的情况,此时会造成大量重复搜索,故考虑在搜索之前对所有已知木棍进行 \(O(n)\) 的预处理操作,将同样长度的木棍合并。

优化六

注意到搜索顺序对搜索树的影响,相比于从更短的已知木棍开始搜索,从更长者开始的情况更少而且更易于剪枝,故改用此顺序。

至此,可以通过本题 30 组数据中的 19 组。

优化七

在上述优化的基础上,注意到选取已知木棍时的单调性,为减小常数可以二分找到第一个不超过剩余长度的已知木棍长度。

优化八

在优化一和优化二的基础上,注意到并非所有满足这两个条件的数都能由若干个已知木棍长度相加得出,故考虑利用 01 背包进行优化。

优化九

注意到在拼凑每一根原始木棍时,如果有解则当前最长的已知木棍必被使用,否则会造成重复搜索,故对此情况特判。

至此,可以通过本题 30 组数据中的 26 组。

优化十(重点)

注意到如果当前已知木棍长度正好等于拼凑成原始木棍所剩的长度,此时该木棍可以等价于更短的已知木棍拼凑出的长度和相同的组合,若此时无解,则使用更短者也不可能有解,故不再继续尝试。

至此,可以通过本题全部数据。

实现代码

#include <bits/stdc++.h>

using std::cin;
using std::cout;
constexpr int MAXN = 70;
int n, m, sum, now;
// m: 合并后长度的种数(优化五)
// sum: 所有长度之和
// now:当前搜索的原始长度
bool ok;  // 是否找到解
int a[MAXN];
int temp[MAXN];         // 用于线性预处理(优化五)
int dict[MAXN];         // 记录序号对应的原始值(优化五)
int pool[MAXN];         // 记录序号对应的值的出现次数用于回溯(优化五)
bool vis[MAXN * MAXN];  // 用于 01 背包(优化八)

void dfs(int cnt, int add, int last) {
    // cnt: 已经成功的组数
    // add: 当前组已有的和
    // last: 上一次选取的值(优化四)
    if (ok)
        return;  // 优化三
    if (add == now)
        ++cnt, add = 0, last = m;
    if (cnt * now == sum) {
        ok = true;
        return;
    }
    int l = 1, r = last;
    int mid;
    while (l <= r) {
        mid = (l + r) >> 1;  // 优化七
        if (add + dict[mid] <= now)
            last = mid, l = mid + 1;
        else
            r = mid - 1;
    }
    for (int i = last; i >= 1; --i) {  // 优化四和优化六
        if (pool[i] == 0)
            continue;
        if (add + dict[i] > now)
            continue;
        --pool[i];
        dfs(cnt, add + dict[i], i);
        ++pool[i];
        if (add + dict[i] == now || add == 0)  // 优化九和优化十
            return;
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    int maxv = 0;
    for (int i = 1; i <= n; ++i) {
        cin >> a[i];
        sum += a[i];
        ++temp[a[i]];  // 优化五
        maxv = std::max(maxv, a[i]);
    }
    vis[0] = true;
    for (int i = 0; i <= n; ++i)
        for (int j = sum - a[i]; j >= 0; --j)
            if (vis[j])
                vis[j + a[i]] = true;  // 优化八
    for (int i = 1; i <= maxv; ++i)
        if (temp[i] > 0) {
            ++m;
            dict[m] = i;
            pool[m] = temp[i];  // 优化五
        }
    for (now = maxv; now <= sum; ++now) {  // 优化一
        if (sum % now != 0 || !vis[now])
            continue;  // 优化二
        ok = false;
        dfs(0, 0, m);
        if (ok)
            break;
    }
    cout << now << '\n';
    return 0;
}

部分思路来自 muduzx62 的视频Kaori 的题解。在此表达感谢。
转载请注明出处。原文地址:https://www.cnblogs.com/na-sr/p/luogu-stick.html

posted @ 2022-05-03 22:32  Na/Sr  阅读(348)  评论(0编辑  收藏  举报