题目
用不超过n个火柴兵出一个数字,(that is, digit 0, 1, 2, 3,4, 5, 6, 7, 8, 9 needs 6, 2, 5, 5, 4, 5, 6, 3, 7, 6 matches),要求组成的数可以被m整除,求最大的m
分析
紫书上一共提及了三种解法,参照紫书上的解析和
https://github.com/aoapc-book/aoapc-bac2nd/blob/master/ch9/UVa12105.cpp (方法3)
https://blog.csdn.net/cy05627/article/details/88748079 (方法1,方法3)
https://www.jianshu.com/p/b84ac7584386 (方法3)
方法1,2
这是一道数位DP(DP的东西好多啊)
第一种
d ( i , j ) d(i,j) d(i,j)代表用i根火柴能够拼成的用m整除余数是j的最大整数,由于用火柴拼成数字是从左到右拼成的,所以每一位数字添加的过程就是状态转移过程,在后面添加一个数字k,状态转移方程就是 d ( i + d i g i t ( k ) , ( j ∗ 10 + k ) % m ) = m a x ( d ( i + d i g i t ( k ) , ( j ∗ 10 + k ) % m ) , d ( i , j ) ∗ 10 + k ) d(i+digit(k),(j*10+k)\%m)=max(d(i+digit(k),(j*10+k)\%m) , d(i,j)*10+k) d(i+digit(k),(j∗10+k)%m)=max(d(i+digit(k),(j∗10+k)%m),d(i,j)∗10+k),所以用刷表法实现,同时涉及到高精度整数的乘法,加法和比较
#include<iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=105,maxm=3005;
int n,m,kase,digits[10]={6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
char dp[maxn][maxm][52];
bool lessthan(char *s1,char *s2){
int l1=strlen(s1),l2=strlen(s2);
if(l1!=l2) return l1<l2;
return strcmp(s1,s2)<0;
}
void update(int i,int j,int i2,int j2,int k){
char temp[52],*ch=dp[i2][j2];
strcpy(temp,dp[i][j]);
//这行代码可以去除重复的前导0(多个前导0)
if(temp[0]=='0') temp[0]='0'+k;
else{
int len=strlen(temp);
temp[len]='0'+k;
temp[len+1]='\0';
}
if(lessthan(ch,temp)) strcpy(ch,temp);
}
int main(void){
kase=0;
while(cin>>n>>m && (n||m)){
//初始化状态,每一个状态是dp[i][j]
for(int i=0;i<=n;++i){
for(int j=0;j<m;++j){
dp[i][j][0]='\0';
}
}
//无效的状态的strlen(state)=0,dp[i][j]='\0'
//有效的状态
dp[0][0][0]='0';
//这里不是很理解,为什么要为dp[0][0]设置成0,虽然的确他是特殊的
//写完后,大概理解了,这样可以表示特殊,这里的dp[0][0]=0说明dp[0][0]状态可行,同时以0开始,说明数位长度是0
//同时,前导0会被删除
//一个问题: 如何保证不会产生前导0?
dp[0][0][1]='\0';
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(dp[i][j][0]=='\0') continue; //无效状态,不进行刷表法,更新下一个状态
for(int k=0;k<10;++k){
if(n-i<digits[k]) continue; //火柴数量不够 n-i是剩下的数量
update(i,j,i+digits[k],(j*10+k)%m,k);
}
}
}
//找到最优结果所对应的火柴数量
int ans=-1;
for(int i=2;i<=n;++i) {
if(dp[i][0][0]!='\0') {
if (ans == -1 || lessthan(dp[ans][0], dp[i][0])) {
ans = i;
}
}
}
printf("Case %d: ",++kase);
if(ans==-1) printf("-1\n");
else printf("%s\n",dp[ans][0]);
}
return 0;
}
//test sample
/*
* 2 1
Case 1: 1
6 3
Case 2: 111
5 6
Case 3: -1
6 2119
Case 4: 0
12 2888
Case 5: 0
*/
第二种
第二种相比起来就比较难想出来,同样是状态
d
(
i
,
j
)
d(i,j)
d(i,j),他代表用m整除余数是j的i位数最少需要多少火柴(无解/初始情况 无穷根火柴) 同样使用刷表法,关于前导0,我开始没有管,假设前导0的存在不会使的答案最大位数的增加,例如有一个n位的非0解,可以增加前导0后,不会有n+1位的带前导0的解存在
d
(
i
+
1
,
(
j
∗
10
+
k
)
%
m
)
=
m
i
n
(
d
(
i
+
1
,
(
j
∗
10
+
k
)
%
m
)
,
d
(
i
,
j
)
+
d
i
g
i
t
s
(
k
)
)
d(i+1,(j*10+k)\%m)=min(d(i+1,(j*10+k)\%m) , d(i,j)+digits(k))
d(i+1,(j∗10+k)%m)=min(d(i+1,(j∗10+k)%m),d(i,j)+digits(k))
//9_22 紫书上的第二种求解方法
//不适用高精度整数计算
//位数增加时,是向右端增加数字
//accepted,但是感觉还是有漏洞,这种做法必须满足最大n位数的答案不会因为可以使用前导0就变成了n+1位数
//感觉不一定,虽然数字大多是满足上述假设的,因为0需要8根火柴,要减少其他数字,拼成0,增加位数,很难,除非全是8
#include<iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=55,maxm=3005;
int digits[10]={6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int dp[maxn][maxm],n,m,kase,remain[10][55],flag=0; //i位数字被m整除后余数为j所需要的最小火柴数量
inline int solve(int a){
while(a>=m) a-=m;
return a;
}
void print(int ans,int rem,int num){
if(ans==0) return;
for(int i=9;i>=0;--i){
int temp=rem-remain[i][ans-1];
temp=solve(m+temp);
if(dp[ans-1][temp]+ digits[i]<=num){
//消除前导0
if(flag || i!=0 || ans==1)
printf("%d",i);
if(i!=0) flag=1;
print(ans-1,temp,num-digits[i]);
return;
}
}
}
int main(void){
kase=0;
// freopen("../UVaCh09/sample9_22/sample9_22_in.txt","r",stdin);
// freopen("../UVaCh09/sample9_22/sample9_22_2_out.txt","w",stdout);
while(cin>>n>>m && (n||m)){
flag=0;
memset(dp,0x3f,sizeof(dp));
memset(remain,0,sizeof(remain));
dp[0][0]=0;
int maxi=n/2;
//预先打表,余数表,i代表其实数字,j代表i后面0的数量,remain(i,j)代表被m整除后的余数
for(int i=1;i<10;++i)
remain[i][0]=solve(i);
for(int i=1;i<10;++i){
for(int j=1;j<=maxi;++j){
remain[i][j]=solve(10*remain[i][j-1]);
}
}
for(int i=0;i<=maxi;++i){
for(int j=0;j<m;++j){
if(dp[i][j]>100) continue;
for(int k=0;k<10;++k){
// if(dp[i][j]+digits[k]>100) continue;
if(dp[i][j]+digits[k]>n) continue; //超过n的大小
// if(k==0 && i==0) continue;
int &t=dp[i+1][(10*j+k)%m];
t=min(t,dp[i][j]+digits[k]);
}
}
}
int ans=-1;
for(int i=maxi;i>=0;--i){
if(dp[i][0]<=n){
ans=i;
break;
}
}
printf("Case %d: ",++kase);
if(ans==0) printf("%d",(n<6)?(-1):0);
else print(ans,0,n);
printf("\n");
}
return 0;
}
虽然通过了,但是觉得不能保证假设一定诚意,于是先去除前导0,计算出最大位数,再用相同的办法求解
//9_22 紫书上的第二种求解方法
//不适用高精度整数计算
//位数增加时,是向右端增加数字
//方法2的修改,使用DP计算位数,使用DP2计算结果
#include<iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=55,maxm=3005;
int digits[10]={6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int dp[maxn][maxm],dp2[maxn][maxm],n,m,kase,remain[10][55],flag=0; //i位数字被m整除后余数为j所需要的最小火柴数量
inline int solve(int a){
while(a>=m) a-=m;
return a;
}
void print(int ans,int rem,int num){
if(ans==0) return;
for(int i=9;i>=0;--i){
int temp=rem-remain[i][ans-1];
temp=solve(m+temp);
if(dp2[ans-1][temp]+ digits[i]<=num){
if(flag || i!=0 || ans==1)
printf("%d",i);
if(i!=0) flag=1;
print(ans-1,temp,num-digits[i]);
return;
}
}
}
int main(void){
kase=0;
// freopen("../UVaCh09/sample9_22/sample9_22_in.txt","r",stdin);
// freopen("../UVaCh09/sample9_22/sample9_22_2_out.txt","w",stdout);
while(cin>>n>>m && (n||m)){
flag=0;
memset(dp,0x3f,sizeof(dp));
memset(dp2,0x3f,sizeof(dp2));
memset(remain,0,sizeof(remain));
dp[0][0]=0;
dp2[0][0]=0;
int maxi=n/2;
//预先打表,余数表,i代表其实数字,j代表i后面0的数量,remain(i,j)代表被m整除后的余数
for(int i=1;i<10;++i)
remain[i][0]=solve(i);
for(int i=1;i<10;++i){
for(int j=1;j<=maxi;++j){
remain[i][j]=solve(10*remain[i][j-1]);
}
}
//DP,求解最大位数,去除前导0
for(int i=0;i<=maxi;++i){
for(int j=0;j<m;++j){
if(dp[i][j]>100) continue;
for(int k=0;k<10;++k){
// if(dp[i][j]+digits[k]>100) continue;
if(dp[i][j]+digits[k]>n) continue; //超过n的大小
if(k==0 && i==0) continue;
int &t=dp[i+1][solve(10*j+k)];
t=min(t,dp[i][j]+digits[k]);
}
}
}
int ans=-1;
for(int i=maxi;i>=0;--i){
if(dp[i][0]<=n){
ans=i;
break;
}
}
printf("Case %d: ",++kase);
if(ans==0){ printf("%d\n",(n<6)?(-1):0); continue;}
//第二步,因为迭代时,需要使用的dp2(i,j)没有前导0的限制
for(int i=0;i<=maxi;++i){
for(int j=0;j<m;++j){
if(dp2[i][j]>100) continue;
for(int k=0;k<10;++k){
if(dp2[i][j]+digits[k]>n) continue; //超过n的大小
int &t=dp2[i+1][solve(10*j+k)];
t=min(t,dp2[i][j]+digits[k]);
}
}
}
print(ans,0,n);
printf("\n");
}
return 0;
}
第三种(代码仓库中的)
可靠的参考:https://www.jianshu.com/p/b84ac7584386
第三种相比起来就比较难想出来,同样是状态
d
(
i
,
j
)
d(i,j)
d(i,j),他代表代表剩余i根火柴时(使用n-i个)能够拼成的用m整除余数是j的最大位数 i是剩余量,不是使用量,同时记录路径,方便逆序输出, 使用填表法添加,所以状态转移方程和以上的完全不一样,相当于是表的顺序是从下往上顺序读取结果
d
(
i
,
j
)
=
d
(
i
−
d
i
g
i
t
(
k
)
,
(
j
∗
10
+
k
)
%
m
)
+
1
d(i,j)=d(i-digit(k),(j*10+k)\%m)+1
d(i,j)=d(i−digit(k),(j∗10+k)%m)+1
//方法3
//从左到右添加数字
#include<iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn=105,maxm=3005;
int n,m,kase,digits[10]={6, 2, 5, 5, 4, 5, 6, 3, 7, 6};
int dp[maxn][maxm],pre[maxn][maxm]; //dp[i][j]代表剩余i根火柴拼出的被m整除余数是j的最大长度
//inline int solve(int a){
// while(a>=m) a-=m;
// return a;
//}
int main(void){
kase=0;
// freopen("../UVaCh09/sample9_22/sample9_22_in.txt","r",stdin);
// freopen("../UVaCh09/sample9_22/sample9_22_2_out.txt","w",stdout);
while(cin>>n>>m && (n||m)){
//初始化
// memset(dp,-1,sizeof(dp));
memset(pre,-1,sizeof(pre));
//填表法
for(int i=0;i<=n;++i){
for(int j=0;j<m;++j){
dp[i][j]=-1;
if(j==0) dp[i][j]=0; //不使用i个火柴,所以允许火柴空置
for(int k=9;k>=0;--k) { //逆序,从大到小
if(i<digits[k]) continue;
int &t=dp[i-digits[k]][(10*j+k)%m];
if(t<0) continue;
if(t+1>dp[i][j]){
dp[i][j] = t + 1;
pre[i][j] = k;
}
}
}
}
printf("Case %d: ",++kase);
if(pre[n][0]==-1) printf("-1");
else{
int i=n,j=0,d,flag=0;
for(int k=dp[i][j];k>0;k=dp[i][j]){
//去除0
d=pre[i][j];
if(d>0 || k==1) flag=1;
if(flag) printf("%d",d);
i-=digits[d];
j=(j*10+d)%m;
}
}
printf("\n");
}
return 0;
}