【USACO2.3.3】和为零

题目

【问题描述】

  请考虑一个由1到 N (N=3, 4, 5 … 9)的数字组成的递增数列:1 2 3 … N。现在请在数列中插入“+”表示加,或者“-”表示减,“ ”表示空白(例如1-2 3就等于1-23),来将每一对数字组合在一起(请不要在第一个数字前插入符号)。计算该表达式的结果并判断其值是否为0。请你写一个程序找出所有产生和为零的长度为N的数列。

【输入格式】

单独的一行表示整数N 。

【输出格式】

按照ASCII码的顺序,输出所有在每对数字间插入“+”, “-”, 或 “ ”后能得到结果为零的数列。

【输入样例】

7

【输出样例】

1+2-3+4-5-6+7
1+2-3-4+5+6-7
1-2 3+4+5+6+7
1-2 3-4 5+6 7
1-2+3+4-5+6-7
1-2-3-4-5+6+7

【数据范围】

  3 <= N <= 9

作者:这次不写略了。。懒得各位再去找

分析

这道题时间复杂度只有3^9,没有什么难的
主要是考一个隔板实现 ,是个子集生成
剪枝的话应该只能加一个最优预测,要用后缀数组预处理,意义不是很大
而且不能逆序,剪枝效果又会减少
这个还是直接生成测试吧,回溯不是很好弄

代码

#include<cstdio>
#include<iostream>
using namespace std;
const int maxn=12;
int n,a[maxn],x[maxn];//ASCLL码顺序:' ','+','-' 
//x[i]表示在第i个元素后面放什么,1,2,3对应 ' ','+','-' 
void Initial(int n)
{
    for(int i=n;i>=1;i--)
    {
        a[i]=i;
    }
    x[0]=2;//手动加隔板
    x[n]=2;
}
char mp(int x)
{
    if(x==2)
        return '+';
    if(x==3)
        return '-';
    return ' ';
}
void out()//判断+输出 
{
    int s=0,tot;
    //两重循环同参数模型
    for(int i=0;i<n;i++)
    {
        if(x[i]==2)
        {
            tot=a[i+1];
            i++;
            while(x[i]==1)
            {
                tot=tot*10+a[i+1];
                i++;
            }
            s+=tot;
            i--;//退位
        }
        if(x[i]==3)
        {
            tot=a[i+1];
            i++;
            while(x[i]==1)
            {
                tot=tot*10+a[i+1];
                i++;
            }
            s-=tot;
            i--;//退位
        }
    }
    if(s==0)
    {
        for(int i=1;i<n;i++)
        {
            printf("%d%c",a[i],mp(x[i]));
        }
        printf("%d\n",a[n]);
    }
}
void run(int k)//准备考虑第k个元素后面放什么
{
    if(k>=n)
    {
        out();
        return;
    }
    for(int i=1;i<=3;i++)
    {
        x[k]=i;
        run(k+1);
    }
}
int main()
{
    //freopen("in.txt","r",stdin);
    scanf("%d",&n);
    Initial(n);
    //printf("%d %d %d",'+','-',' ');
    run(1);
    return 0;
}

USA实现依旧不那么容易
这道题和基础隔板的延伸在于隔板是有意义的,而且这个隔板的意义范围是对后面的元素,因此生成测试的方法里面就出现了一个很重要的模型——两重循环同参数模型 (是我瞎BB的)
这个模型时间复杂度是o(n),最常见的操作就是退位操作 (还是我瞎BB的) ,有障碍了要退回去让新的操作来再次读入这个障碍,提炼出这个模型之后就不要因为觉得这个实现诡异而害怕了

番外

根据我的性格我依旧是不会允许写这么基础的代码的,因此我又实现了回溯算法加上预测剪枝,剪枝大概减少了百分之二十的时间

#include<cstdio>
#include<iostream>
using namespace std;
const int maxn=12;
int n,a[maxn];//ASCLL码顺序:' ','+','-' 
char x[maxn];
int sum[maxn][maxn];
void Initial(int n)
{
    for(int i=1;i<=n;i++)
    {
        a[i]=i;
        for(int j=i;j<=n;j++)
        {
            sum[i][j]=sum[i][j-1]*10+j;
        }
    }
} 
void run(int k,int s)//从第k个元素开始考虑,已经得到的和为1 
{
    if(k>=n+1)
    {
        if(s==0)
        {
            cout<<1;
            for(int i=2;i<=n;i++)
            {
                cout<<x[i]<<i;
            }
            cout<<endl;
        }
        return ;
    }
    x[k]='+';
    for(int i=n;i>=k+1;i--)
    {
        for(int j=k+1;j<=i;j++)
        x[j]=' ';
        run(i+1,s+sum[k][i]);
    }
    run(k+1,s+k);
    if(k==1)
        return;
    x[k]='-';
    for(int i=n;i>=k+1;i--)
    {
        for(int j=k+1;j<=i;j++)
        x[j]=' ';
        run(i+1,s-sum[k][i]);
    }
    run(k+1,s-k);
}
int main()
{
    //freopen("in.txt","r",stdin);
    scanf("%d",&n);
    Initial(n);
    run(1,0);
    return 0;
}

这次实现稍微把x意义改了,改成i元素前面那个元素的范围,也没有用a数组和映射函数,比较贴合这道题目了,效率比之前高了些,但其实都是几十毫秒的事情了。
为了实现字典序采用逆序枚举的方法

实现中有个问题就是特殊考虑问题,本来想统一化的但是一直都没有办法规避,原因是因为n个数字只有n-1个符号,因为之前的习惯一直没有进行过特殊考虑(行末空格自动忽略的原因),想了一个办法利用行末空格,但是和意义不符合,还是特殊考虑了

再分析

这道题用的子集生成算法,根据题意我们再看看
我们可以把数分成加或减的数,必须要求加的数为tot/2,但是根据这个并没有想出更好的算法,根据原理其实也是子集生成,欢迎补充其它思路

收获与知识点

  1. 两重循环单参数模型->退位操作
  2. 预测剪枝
  3. 分析题目合理设置函数、数组意义,不要拘泥
  4. 排序的有序性
  5. 不要害怕特殊考虑
  6. USACO的题不卡时间复杂度考实现很刁钻
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值