【ACWing】167. 木棒

题目地址:

https://www.acwing.com/problem/content/169/

乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 50 50个长度单位。然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棍的长度都用大于零的整数表示。

输入格式:
输入包含多组数据,每组数据包括两行。第一行是一个不超过 64 64 64的整数 n n n,表示砍断之后共有多少节木棍。第二行是截断以后,所得到的各节木棍的长度。在最后一组数据之后,是一个零。

输出格式:
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

数据范围:
n ≤ 64 n\le 64 n64,数据保证每一节木棍的长度均不大于 50 50 50

思路是DFS。先从小到大枚举木棒的长度,然后再直接按次序枚举每个木棒是怎么由小木棍拼成的,即,按次序将小木棍加入当前的木棒中,拼出一个木棒后,再枚举下一个木棒,以此类推,如果中途发现当前木棒拼不出来了,则返回false;当第一次枚举出可以拼出的木棒长度时,这个长度就是答案。这里注意几个剪枝:
1、枚举木棒长度的时候,可以从最长的那根小木棍的长度开始枚举,因为小于这个长度是不可能有解的。显然木棒长度的最大值就是所有小木棍的长度之和 s s s
2、枚举木棒长度的时候,只需枚举 s s s的约数;
3、显然这里的枚举是组合枚举,并不是排列枚举,所以我们可以人为规定次序,每次枚举的时候只从下标从小到大进行枚举;
4、优化搜索顺序:为了使得分支数尽量少,可以按照小木棍的长度从大到小枚举,显然当先枚举长的小木棍的时候,当前木棍剩余较少,方案也会变少,这样有利于剪枝。在代码里只需对小木棍长度排个序即可。注意,这样搜索出的答案,一定是一个字典序从大到小的方案,组与组之间字典序上升,并且组内部字典序也上升(长度下降);
5、如果在某个位置(即枚举到第 u u u个木棒的第 k k k个小木棍的长度了),枚举一个长 x x x的小木棍(设其下标为 i i i),发现无解,那么接下来在当前位置,所有长 x x x的小木棍放在这个位置都不会有解。可以用反证法,如果某个长 x x x的小木棍放在这里有解,那么其下标 j > i j>i j>i,可以将其与下标 i i i的那根小木棍进行交换,仍然能得到一个合法解,这就矛盾了;
6、如果在枚举第 u u u个木棒,长度 x x x的小木棍放在最左边的时候无解,那么可以直接返回false,本层枚举无解。也可以用反证法,如果该小木棍放在后面某个位置的时候有解,那么这个位置对应的小组的最左边的小木棍长度如果等于 x x x,那把这个小组整个换过去应该是有解的,矛盾了;如果最左边的小木棍长度不等于 x x x,则一定小于 x x x(因为要保证字典序),这也矛盾,因为小组内部长度是递减的。
7、如果在枚举第 u u u个木棒,长度 x x x的小木棍放在最右边的时候无解,那么可以直接返回false,本层枚举无解。也可以用反证法,如果该小木棍放在后面某个位置的时候有解,那么可以将第 u u u个木棒最右边长度为 x x x的后缀和那个小木棍交换(因为当前的小组一定会被填满的),这样就得到了一个合法解,与该小木棍放在最右边的时候无解矛盾。

代码如下:

#include 
#include 
#include 
using namespace std;

const int N = 70;
int n;
// len存每个木棒的长度,sum存总长度
int a[N], len, sum;
// 存每个小木棍是否被用过
bool st[N];

// u表示在拼第几个木棒(从0开始计数),l表示当前木棒已经拼了多少长度,start表示从哪个小木棍开始枚举
bool dfs(int u, int l, int start) {
	// 如果拼完了所有木棒,说明找到了一个解,则返回true
    if (u * len == sum) return true;
    // 如果没拼完所有木棒,但是拼好了当前的木棒,则继续拼下一个木棒
    if (l == len) return dfs(u + 1, 0, 0);

    for (int i = start; i < n; i++) {
    	// 略过用过的小木棍
        if (st[i]) continue;
        // 略过用之就会超出长度限制的小木棍
        if (l + a[i] > len) continue;
        // 同样长度的小木棍只枚举第一个
        if (i > start && !st[i - 1] && a[i] == a[i - 1]) continue;

        st[i] = true;
        if (dfs(u, l + a[i], i + 1)) return true;
        st[i] = false;

		// 如果当前小木棍是作为拼当前木棒的最左或最右的木棍的,且没成功,那可以断言无解
        if (!l || l + a[i] == len) return false;
    }

	// 如果可供选择的小木棍没有一个能得到合法解,则说明无解,返回false
    return false;
}

int main() {
    while (cin >> n, n) {
        sum = len = 0;
        // 注意这里如果无解的话,st是会恢复原状的。所以只需要在一开始的时候set一次即可
        memset(st, 0, sizeof st);
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            sum += a[i];
        }
		
		// 将所有小木棍按长度从大到小排序
        sort(a, a + n, greater<int>());
        
        // 直接从最长的小木棍长度开始枚举,直到第一次能拼出所有木棒即输出答案
        for (len = a[0]; len <= sum; len++)
        	// 只枚举sum的约数
            if (sum % len == 0 && dfs(0, 0, 0)) {
                printf("%d\n", len);
                break;
            }
    }

    return 0;
}

时间复杂度指数级,空间 O ( n ) O(n) O(n)

也可以依次枚举每个小木棍放在哪个组里,可以放在之前某个非空组里,也可以新开一个组。剪枝方面见注释。代码如下:

#include 
#include 
#include 
using namespace std;

const int N = 70;
int n;
int a[N], len, sum;
// s[i]是第i组已经拼出的长度,g[i]是下标为i的小木棍放在了的组的下标
int s[N], g[N];

// u是当前枚举到了的小木棍的下标,cnt是已经有多少个非空组
bool dfs(int u, int cnt) {
	// 如果枚举完了,看一下cnt是否是sum / len,
	// 如果是,说明得出了一个解,返回true
    if (u == n) {
        return cnt * len == sum;
    }
    
    // 如果开的组数过大,说明无解,返回false
    if (cnt > sum / len) return false;
    
    int start = 0;
    // 如果当前小木棍和上一个小木棍等长,则其从上一个小木棍所在组及其之后开始枚举
    if (u && a[u] == a[u - 1]) start = g[u - 1];
    
    for (int i = start; i <= cnt; i++) {
        if (s[i] + a[u] > len) continue;
        
        s[i] += a[u];
        g[u] = i;
        if (dfs(u + 1, cnt + (i == cnt ? 1 : 0))) return true;
        s[i] -= a[u];
        
        // 如果a[u]放在首尾无解,则该分支无解
        if (!s[i] || s[i] + a[u] == len) return false;
    }
    
    return false;
}

int main() {
    while (cin >> n, n) {
        sum = len = 0;
        memset(s, 0, sizeof s);
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
            sum += a[i];
        }
            
        sort(a, a + n, greater<int>());
        for (len = a[0]; len <= sum; len++)
            if (sum % len == 0 && dfs(0, 0)) {
                printf("%d\n", len);
                break;
            }
    }

    return 0;
}

时空复杂度一样。

你可能感兴趣的:(AC,搜索与图论,算法,c++,深度优先)