小木棍(含剪枝原理详细数学证明)

传送门

题干大意:

乔治有一些同样长的小木棍,他把这些木棍随意砍成几段,直到每段的长都不超过50。现在,他想把小木棍拼接成原来的样子,但是却忘记了自己开始时有多少根木棍和它们的长度。给出每段小木棍的长度,编程帮他找出原始木棍的最小可能长度。

输入描述:

第一行为一个单独的整数N表示砍过以后的小木棍的总数。第二行为N个用空格隔开的正整数,表示N根小木棍的长度。

输出描述:

输出仅一行,表示要求的原始木棍的最小可能长度。

备注:

1≤N≤60

题目分析:

  1. 按木棍长度从大到小排序,便于后续处理
  2. 因为直接求不出来,所以枚举长度len(范围:a[0]~sum),化查询为判断
  3. 由此得出tle的dfs:
#define fo(i,n) for(int i=0;i<n;++i)
bool dfs(int num,int len,int rest){
    //还剩几根小棍没用,枚举的木棍长度,当前在拼的木棍还剩多长 
    if( (rest==0)&&(num==0) ) return true;
    if(rest==0) rest=len;
    fo(i,0,n){
        if(v[i])continue;//如果小木棍用过了
        if(a[i]>rest)continue;//如果这根小木棍不能用
        v[i]=true;
        if(dfs(num-1,len,rest-a[i])) return true;
        v[i]=false;//回溯(如果这根小棍不能拼成的话,就把这根小棍拆了)
   } 
    return 0;
} 

不想TLE,于是开始优化之路

  1. 回溯后加上代码:if(len==rest)break;(因为这个木棒是去拼某个len的第一个木棒)
  2. 难点,下文有具体解释: 如果回溯后发现这个木棒是去拼某个len的第最后一个木棒,则该len一定不合法,即if(a[i]==rest)break;
  3. 跳过相同木棒,即while(a[i]==a[i+1]) i++;
  4. 搜索顺序的优化:我们可以按照小木棍的长度进行排序,从大到小,这样比其它顺序快(大块一定比小块需要搜索的次数少)
  5. 如果当前在拼的木棍用了x根棍子,那么之后的dfs从x+1开始找就行(很巧妙),但注意如果更新了当前在拼的木棍,之后的dfs还要从第一根棍子开始找
  6. 写一下枚举len时的优化:
sort(a,a+n,cmp);//从大到小排序
    for(int i=a[0];i<=sum;i++)
        if(sum%i==0){
            if(dfs(n,i,0,0)) {
                cout<<i<<endl;
                exit(0);//注意是退出程序
            }
        }//循环节保障>1根棒,那么初始每个棍长一定<=sum/2
    printf("%d\n",sum);//一根棒时相加即可

第二点较难,给出具体证明:

用 反 证 法 : ( 有 贪 心 的 感 觉 ) 用反证法:(有贪心的感觉) ()

当进行到 ∑ a 0 , a 1 , a 2 , … i , … j , … m , … n , … \sum _{a_{0}}, a_{1}, a_{2}, …i,… j, …m, …n,… a0,a1,a2,i,j,m,n,时,假设恰有目标长度 s u m = ∑ a 0 + a 1 , sum=\sum _{a_{0}}+a_{1}, sum=a0+a1, a 1 a_{1} a1就是去拼某个len的第最后一个木棒)但其为失败搜索,则 ∑ a 0 + a 1 \sum _{a_{0}}+a_{1} a0+a1之后应当退出

假设 a 1 = = m + n + ∑ 1 a_{1}==m+n+\sum _{1} a1==m+n+1 ∑ 1 \sum _{1} 1代表可能的其它数据),将 a 1 a_{1} a1替换为 m + n + ∑ 1 m+n+\sum _{1} m+n+1,若有成功搜索,则肯定有某个和为 ∑ a 0 \sum _{a_{0}} a0,可以找 i , j , … i,j , … ij,满足 ∑ a 0 = i + j + ∑ 2 \sum _{a_{0}}=i+j+\sum _{2} a0=i+j+2 ( ∑ 2 \sum _{2} 2同样代表可能的其它数据)

但是它在 a 1 a_{1} a1使用完之后就应该搜索到了一种情况: s u m = m + n + ∑ 1 + i + j + ∑ 2 sum=m+n+\sum _{1}+i+j+\sum _{2} sum=m+n+1+i+j+2,这就产生了矛盾

所以如果使用更短的小木棍,即使存在某个也能填满该原始木棍的方法,该方法也一定不会比用大木棍更有希望获得可行解

AC代码:

#include<bits/stdc++.h>
#define LL long long
#define fo(i,a,b) for(int i=a;i<b;i++)
#define js ios::sync_with_stdio(false);cin.tie(0); cout.tie(0)
using namespace std;
int v[65];//visit:标记小棍
int a[65];//小棍长度
int n,sum; 
bool dfs(int num,int len,int rest,int now){
//还剩几根小棍没用,枚举的木棍长度,当前在拼的木棍还剩多长,去拼某个len的第几位 
    if( (rest==0)&&(num==0) ) return true;
    if(rest==0) rest=len,now=0;
    for(int i=now;i<n;i++){
        if(v[i])continue;
        if(a[i]>rest)continue;
        v[i]=true;
        if(dfs(num-1,len,rest-a[i],i+1)) return true;
        v[i]=false;//如果这根小棍不能拼成的话,就把这根小棍拆了
        if(a[i]==rest)break;//剪枝(去拼某个len的第最后一个木棒) 
        if(len==rest)break;//剪枝(去拼某个len的第一个木棒)
        while(a[i]==a[i+1]) i++;//剪枝(跳过相同木棒)
    } 
    return 0;
} 
bool cmp(int a,int b){return a>b;}
int main(){
    scanf("%d",&n); 
    for(int i=0;i<n;i++){
        scanf("%d",&a[i]);  
        sum+=a[i];
    }
    sort(a,a+n,cmp);//从大到小排序
    for(int i=a[0];i<=sum;i++)
        if(sum%i==0){
            if(dfs(n,i,0,0)) {
                cout<<i<<endl;
                exit(0);
            }
        }
    printf("%d\n",sum);
    return 0;   
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值