zoukankan      html  css  js  c++  java
  • P1120 小木棍 [数据加强版] 题解

    CSDN同步

    原题链接

    简要题意:

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

    暴力出奇迹

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

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

    首先考虑 ( exttt{dfs}),枚举长木棍的长度,然后用 ( exttt{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) 个。

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

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

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

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

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

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

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

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

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

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

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

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

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

    3. 可以先从 (1) ~ (lfloor frac{s}{2} floor),如果最终不合法再可以输出 (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
    ",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
    ",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
    ",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
    ",s);
    	return 0;
    }
    
    
  • 相关阅读:
    javascript学习笔记(一):基础、输出、注释、引用、变量、数据类型
    black-hole《XSS的原理分析与解剖》阅读笔记
    《xss跨站脚本剖析与防御》实验笔记
    XSS和CSRF的区别
    前端常用插件、工具类库汇总(下)
    前端常用插件、工具类库汇总(上)
    前端特效【第03期】|果汁混合效果-上
    玩转 React 【第03期】:邂逅 React 组件
    前端修炼の道 | 如何成为一名合格前端开发工程师?
    前端特效【第02期】|多功能提交按钮
  • 原文地址:https://www.cnblogs.com/bifanwen/p/12603970.html
Copyright © 2011-2022 走看看