题目
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…