快速求和

来源:互联网 发布:淘宝店铺模板修改 编辑:程序博客网 时间:2024/05/22 01:28

题目

1.1题目描述

给定一个数字字符串,用最少次数的加法让字符串等于一个给定的目标数字。每次加法就是在字符串的某个位置插入一个加号。在需要的所有加号都插入后,就象做普通加法那样来求值。 例如,考虑字符串”12”,做0次加法,我们得到数字12。如果插入1个加号,我们得到3。因此,这个例子中,最少用1次加法就得到数字3。 再举一例,考虑字符串”303”和目标数字6,最佳方法不是”3+0+3”,而是”3+03”。能这样做是因为1个数的前导0不会改变它的大小。 写一个程序来实现这个算法。

1.2输入

第1行:1个字符串S和1个整数N。S和N用空格分隔。

1.3输出

第1行:1个整数K,表示最少的加法次数让S等于N。如果怎么做都不能让S等于N,则输出-1。

1.4样例输入输出

样例输入#1

2222 8

样例输出#1

3

样例输入#2

2222222222222222222222222222222222222222 80

样例输出#2

39

数据范围

本题分为两个版本
版本1 1<=length(S)<=10,N<=10^9-1
版本2 1<=length(S)<=40,N<=10^5

题解

2.1 开局直接搜(bao)索(li)

先来看看版本1吧…Emmmm…数据水到和谐啊2333….
很明显的深搜然而本蒟蒻还是看错了题
直接仿造子集问题,生成全部可能的子集再判断即可
代码如下

#include<iostream>#include<cstdio>#include<algorithm>#include<climits>#include<cstring>using namespace std;char s[25];int f[25][25];int re,ans=INT_MAX;void dfs(int deep,int last,int num,int sum){    if(deep==strlen(s+1))    {        sum+=f[last+1][strlen(s+1)];        if(sum==re)ans=min(ans,num);            return ;    }    dfs(deep+1,last,num,sum);//不选s[deep]    dfs(deep+1,deep,num+1,sum+f[last+1][deep]);//选s[deep]}int main(){    scanf("%s",s+1);scanf("%d",&re);    for(int i=1;i<=strlen(s+1);i++)f[i][i]=s[i]-'0';    for(int i=1;i<=strlen(s+1);i++)        for(int j=i+1;j<=strlen(s+1);j++)        {f[i][j]=f[i][j-1]*10+f[j][j];        if(j-i>9)f[i][j]=INT_MAX;}    dfs(1,0,0,0);    if(ans==INT_MAX)printf("-1");    else printf("%d",ans);}

2.2继续剪枝优化

就这样,水到不正常的版本1就AC了…然而还有版本2呢233…这时我们就应该想到剪枝了~
我们知道剪枝分为可行性剪枝和最优性剪枝。
恰巧的是,这道题两个都可以使用。

可行性剪枝:本问题的主参数i,如果从last+1到i的距离太大,生成的整数加上之前的部分和sum,已经超过n,则退出枚举。因为继续往后枚举,生成的数会更大,更不会符合要求。
最优性剪枝:假设之前已经得到的可行解记录在ans中,如果在当前阶段放的加号个数p已经大于等于ans,那么这次搜索再继续下去是不可能找到更加优秀的解,因此必须提前退出。

加了这两个剪枝后,代码效率明显变高了。
代码如下

#include<iostream>#include<cstdio>#include<algorithm>#include<climits>#include<cstring>using namespace std;char s[45];int f[45][45];int re,ans=INT_MAX;void dfs(int deep,int last,int num,int sum){    if(sum+f[last+1][deep]>re) return;//可行性剪枝    if(num>=ans) return; //最优性剪枝    if(deep==strlen(s+1))    {        sum+=f[last+1][strlen(s+1)];        if(sum==re)ans=min(ans,num);            return ;    }    dfs(deep+1,last,num,sum);//不选s[deep]    dfs(deep+1,deep,num+1,sum+f[last+1][deep]);//选s[deep]}int main(){    scanf("%s",s+1);scanf("%d",&re);    for(int i=1;i<=strlen(s+1);i++)f[i][i]=s[i]-'0';    for(int i=1;i<=strlen(s+1);i++)        for(int j=i+1;j<=strlen(s+1);j++)        {f[i][j]=f[i][j-1]*10+f[j][j];        if(j-i>9)f[i][j]=INT_MAX;}    dfs(1,0,0,0);    if(ans==INT_MAX)printf("-1");    else printf("%d",ans);}

这样做可以过版本2的大部分数据。
什么???只能过大部分数据??
试试这个数据你就知道了

样例输入#3
2222222222222222222222222222222222222222 9999
样例输出#3
-1

是不是有一种慢到绝望的感觉?
分析算法可以发现,当剪枝遇到无解的情况时,剪枝搜索又退化成了普通的暴力搜索了。
虽然可能可以用clock()卡过这题然而并不支持这种做法...
现在,能走的路也只有记忆化搜索了…

2.3记忆化搜索

其实记忆化也就是优化最优性剪枝啦~
通过观察我们可以发现我们不仅仅可以把大于ans的方案剪掉,我们甚至可以定义一个f[i][j]表示走到i格时离目标goal的距离j时所花费的最小代价。也就是走到i,j时如果花费大于f[i][j]时,可将其直接剪掉。
换种说法,也等价于到达i,j时直接搜索后方的数,在满足条件的方案中选出最小值即可。
另外2.2的可行性剪枝也可以继续使用哦~
代码实现

#include<iostream>#include<cstdio>#include<algorithm>#include<climits>#include<cstring>using namespace std;char s[45];int sum[45][45];int f[45][100005];int re;int dp(int deep,int goal){    int minn_ans=INT_MAX,ksum;    if(deep>strlen(s+1))    {if(goal==0)return 0;else return INT_MAX;}    if(f[deep][goal]!=-1)return f[deep][goal];    for(int j=deep;j<=strlen(s+1);j++)    {        int ksum=sum[deep][j];        if(ksum>goal)break;//剪枝        int kkk=dp(j+1,goal-ksum);        minn_ans=min(minn_ans,kkk);    }    if(minn_ans<INT_MAX)f[deep][goal]=minn_ans+1;    else f[deep][goal]=INT_MAX;        return f[deep][goal];}int main(){    scanf("%s",s+1);scanf("%d",&re);    for(int i=1;i<=strlen(s+1);i++)        for(int j=0;j<=re;j++)f[i][j]=-1;    for(int i=1;i<=strlen(s+1);i++)sum[i][i]=s[i]-'0';    for(int i=1;i<=strlen(s+1);i++)        for(int j=i+1;j<=strlen(s+1);j++)        {sum[i][j]=sum[i][j-1]*10+sum[j][j];        if(j-i>9)sum[i][j]=INT_MAX;}    int ans=dp(1,re);    if(ans==INT_MAX)printf("-1");    else printf("%d",ans-1);}

这样,这道题目就解决了。
然而已经有了记忆化搜索,DP行不行呢?

4.4 DP出现

显然,DP当然行啦~
这次我们反着定义状态
定义f[i][k]表示前i个数中凑到整数k的最少加号数。
g[i][j]表示第i位到第j位形成的数,可以预处理出来。与之前的预处理相同。
枚举前一个加号的位置j,然后从f[i][k-g[j+1][i]]转移过来,形成递推。即
f[i][k]=min(f[i][k], f[i][k-g[j+1][i]]+1)
或者把状态转移方程写成:
f[i][k+g[j+1][i]]=min(f[i][k+g[j+1][i]],f[j][k]+1)
这样,DP就可以轻松地解决这道题啦~
代码如下

#include<iostream>#include<cstdio>#include<algorithm>#include<climits>#include<cstring>using namespace std;char s[45];int sum[45][45];int f[45][100005];int re;int main(){    scanf("%s",s+1);scanf("%d",&re);    for(int i=0;i<=strlen(s+1);i++)//注意是从0开始         for(int j=0;j<=re;j++)f[i][j]=INT_MAX;    for(int i=1;i<=strlen(s+1);i++)sum[i][i]=s[i]-'0';    for(int i=1;i<=strlen(s+1);i++)        for(int j=i+1;j<=strlen(s+1);j++)        {sum[i][j]=sum[i][j-1]*10+sum[j][j];        if(j-i>9)sum[i][j]=INT_MAX;}   f[0][0]=-1;    for(int i=1;i<=strlen(s+1);i++)        for(int j=1;j<=i;j++)        {            if(sum[i-j+1][i]>re)break;                for(int k=0;k<=re;k++)                    if(f[i-j][k]!=INT_MAX)                     {                        if(k+sum[i-j+1][i]>re)break;//仍然是剪枝                         f[i][k+sum[i-j+1][i]]=min(f[i][k+sum[i-j+1][i]],f[i-j][k]+1);                    }        }    if(f[strlen(s+1)][re]==INT_MAX)printf("-1");    else printf("%d",f[strlen(s+1)][re]);}

就这样,这道题就成功解决了。
当然你也可以试试怎样用clock卡过这道题了233…

原创粉丝点击