P1120 小木棍 [数据加强版] 题解

CSDN同步

原题链接

简要题意:

把若干 \(\leq 50\) 的小木棍拼成若干长度相同的长木棍(一个小木棍也可以作为一根长木棍)。求可以拼成的长木棍的最小长度。

暴力出奇迹

一看数据范围,\(n \leq 65\).

这一看就是指数级复杂度 我还没见过什么 \(O(n^5)\) 的算法。。

首先考虑 \(\texttt{dfs}\),枚举长木棍的长度,然后用 \(\texttt{dfs}\) 进行暴力枚举当前的小木棍分给哪一组(可以算出组数的),验证即可。

对这种最 卑劣 不太行的搜索,下面开始大力剪枝:

  1. 假设木棍总长度为 \(s\),则长木棍的长度 \(x\) 应满足 \(x | s\),否则分不成整数组。所以不是 \(s\) 因数的情况可以直接跳过。如果小于小木棍的最长长度也可以跳过,因为最长的木棍没法拼了。

  2. 如果你现在需要长度 \(5\) 的木棍拼成一个长木棍,你会选 \(5\) 一根 还是 \(2 + 3\) 两根呢?

显然选 \(5\). 因为小的木棍永远比大木棍灵活,大木棍能拼的小木棍也能,但小木棍能拼的大木棍未必。

所以将木棍长度最大到小排序,优先选大的。

  1. 如果当前木棍拼接失败,不再尝试相同长度的木棍,直接跳到后面一个不同的。为什么呢?假设给定数据:
10
6 6 6 3 3 3 3 3 3 3

总和为 \(30\),你在验证 \(10\).

你拼上一个 \(6\),又拼上一个 \(3\),不行;然后你就把后面的 \(3\) 一个一个全都尝试一遍发现不行。

然后你又试第 \(2\)\(6\),又是 \(7\)\(3\) 枚举一遍,然后第 \(3\) 个。

但是,状态的大量重复 导致根本没有必要搜这么多次。

我当前长度都失败,换个相同长度的不也是失败?

所以,用 \(\texttt{nxt_i}\) 表示与 \(a_i\) 不同的(排序后)编号 \(\geq i\) 的最小编号(\(\texttt{nxt_n=n}\)),失败后直接往 \(\texttt{nxt}\) 上面跳就可以了。

  1. 假设当前离一个长木棍的长度相差 \(\text{no}\),那么直寻找 \(\leq no\) 的。

你可能说这循环扫一遍?但是经过排序的数组明明可以二分

所以,又优化了一点。(据我所知,这个优化不写应该也可以 \(\text{AC}\) 吧,但是写了比较好)

  1. 每次木棍是否用过的标记数组,不需要每次 \(\texttt{memset}\),可以在搜索中:选当前木棍则先标记,然后进入下一层;下一层结束之后再取消标记,为的是不要影响再一次回溯。这样可以用搜索同级时间维护。(这是一个很常用的技巧)

  2. 如果以及发现,每个木棍都拼了上去,那么不用再回溯了,直接输出答案结束整个程序。(其实也不用一层层的退出,直接结束程序,用 \(\text{exit(0)}\)

  3. 如果当前木棍接上之后正好能拼成一根长木棍,然后经过回溯发现失败了,那么就把之前拼在这个长木棍上的依次废掉,重新拼。为什么呢?

因为,我们是先选长木棍,肯定比短木棍来拼要优(上述已经说明),所以如果长木棍都拼不了,那当前这条长木棍的拼法就是不对的。所以需要改变之前的拼法。

为什么是“依次”呢?假设我们先废掉了一根木棍,然后重新拼;这时如果合法就直接结束了,否则回溯到此不合法。然后就把再之前的木棍废掉,依次类推。

  1. 如果已经成功拼成了(组数-1) 根木棍,由于整除性,剩下的部分肯定能拼成了。所以可以直接判定正确。

  2. 按照管理员大大的暗示\(>50\) 的直接去掉即可。

  3. 可以先从 \(1\) ~ \(\lfloor \frac{s}{2} \rfloor\),如果最终不合法再可以输出 \(s\).(因为 \(s\) 肯定合法)其实可以先枚举因数,但是不会有多少的优化。

有了这些剪枝,我们就可以愉快地 暴力碾标算 啦!

时间复杂度:未知。(难以分析)

实际得分:\(100pts\).(反正对了就行)

#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;

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

int n,m,a[66],s=0,len;
int nxt[66]; bool ok,h[66];

inline void dfs(int dep,int last,int no) {
	if(!no) { //需要重新开始拼一根
		if(dep==m) {
			printf("%d\n",len); exit(0); //退出
		} int wz;
		for(int i=1;i<=a[0];i++)
			if(!h[i]) {wz=i;break;}
		h[wz]=1; //第一个没有拼的,就把它拼上去(因为长的比短的优)
		dfs(dep+1,wz,len-a[wz]);
		h[wz]=0; if(ok) {
			printf("%d\n",len); exit(0);
		} //退出
	} int l=last+1,r=a[0];
	while(l<r) {
		int mid=(l+r)>>1;
		if(a[mid]<=no) r=mid;
		else l=mid+1; //二分可以拼的最长木棍
	} for(int i=l;i<=a[0];i++)
		if(!h[i]) {
			h[i]=1; dfs(dep,i,no-a[i]);
			h[i]=0; if(ok) {
				printf("%d\n",len); exit(0); //直接退出
			} if(no==a[i] || no==len) return; //发现拼不了
			i=nxt[i]; if(i==a[0]) return; //说明整根拼不上
		}
}

int main(){
	n=read();
	for(int i=1,t;i<=n;i++) {
		t=read(); //把 >50 的剪掉
		if(t<=50) a[++a[0]]=t,s+=t;
	} sort(a+1,a+1+a[0]); reverse(a+1,a+1+a[0]); //从大到小排序
	nxt[a[0]]=a[0]; for(int i=a[0]-1;i>0;i--)
		nxt[i]=(a[i]==a[i+1])?nxt[i+1]:i; //后面和自己不相同的最小编号
	for(len=a[1];len<=(s>>1);len++) { //位运算再次优化
		if(s%len) continue; //整除
		m=s/len; ok=0; h[1]=1; //标记
		dfs(1,1,len-a[1]); h[1]=0;
	} printf("%d\n",s);
	return 0;
}

posted @ 2020-03-31 11:29  bifanwen  阅读(202)  评论(0编辑  收藏  举报