UVA - 12105 Bigger is Better

题目

用不超过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),(j10+k)%m)=max(d(i+digit(k),(j10+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,(j10+k)%m)=min(d(i+1,(j10+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(idigit(k),(j10+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;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值