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

博客园同步

原题链接

简要题意:

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

暴力出奇迹

一看数据范围, n ≤ 65 n \leq 65 n65.

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

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

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

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

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

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

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

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

总和为 30 30 30,你在验证 10 10 10.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

实际得分: 100 p t s 100pts 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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值