P1120 小木棍 [数据加强版] 题解
简要题意:
把若干 \(\leq 50\) 的小木棍拼成若干长度相同的长木棍(一个小木棍也可以作为一根长木棍)。求可以拼成的长木棍的最小长度。
暴力出奇迹
一看数据范围,\(n \leq 65\).
这一看就是指数级复杂度 我还没见过什么 \(O(n^5)\) 的算法。。
首先考虑 \(\texttt{dfs}\),枚举长木棍的长度,然后用 \(\texttt{dfs}\) 进行暴力枚举当前的小木棍分给哪一组(可以算出组数的),验证即可。
对这种最 卑劣 不太行的搜索,下面开始大力剪枝:
-
假设木棍总长度为 \(s\),则长木棍的长度 \(x\) 应满足 \(x | s\),否则分不成整数组。所以不是 \(s\) 因数的情况可以直接跳过。如果小于小木棍的最长长度也可以跳过,因为最长的木棍没法拼了。
-
如果你现在需要长度 \(5\) 的木棍拼成一个长木棍,你会选 \(5\) 一根 还是 \(2 + 3\) 两根呢?
显然选 \(5\). 因为小的木棍永远比大木棍灵活,大木棍能拼的小木棍也能,但小木棍能拼的大木棍未必。
所以将木棍长度最大到小排序,优先选大的。
- 如果当前木棍拼接失败,不再尝试相同长度的木棍,直接跳到后面一个不同的。为什么呢?假设给定数据:
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}\) 上面跳就可以了。
- 假设当前离一个长木棍的长度相差 \(\text{no}\),那么直寻找 \(\leq no\) 的。
你可能说这循环扫一遍?但是经过排序的数组明明可以二分 。
所以,又优化了一点。(据我所知,这个优化不写应该也可以 \(\text{AC}\) 吧,但是写了比较好)
-
每次木棍是否用过的标记数组,不需要每次 \(\texttt{memset}\),可以在搜索中:选当前木棍则先标记,然后进入下一层;下一层结束之后再取消标记,为的是不要影响再一次回溯。这样可以用搜索同级时间维护。(这是一个很常用的技巧)
-
如果以及发现,每个木棍都拼了上去,那么不用再回溯了,直接输出答案结束整个程序。(其实也不用一层层的退出,直接结束程序,用 \(\text{exit(0)}\))
-
如果当前木棍接上之后正好能拼成一根长木棍,然后经过回溯发现失败了,那么就把之前拼在这个长木棍上的依次废掉,重新拼。为什么呢?
因为,我们是先选长木棍,肯定比短木棍来拼要优(上述已经说明),所以如果长木棍都拼不了,那当前这条长木棍的拼法就是不对的。所以需要改变之前的拼法。
为什么是“依次”呢?假设我们先废掉了一根木棍,然后重新拼;这时如果合法就直接结束了,否则回溯到此不合法。然后就把再之前的木棍废掉,依次类推。
-
如果已经成功拼成了(组数-1) 根木棍,由于整除性,剩下的部分肯定能拼成了。所以可以直接判定正确。
-
按照管理员大大的暗示把 \(>50\) 的直接去掉即可。 -
可以先从 \(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;
}