题意:给定一个长度为2n的串,每个字符或者是数字,或者是?。?的位置也应该是一个字符表示相应的数字,但是看不清了。问把?还原成数字,使得前n个数字的乘积等于后n个数字的乘积的还原方法有多少种,不等于的又有多少种?
输入:
2
2??3
输出:
4
96
样例解释:两个问号填数字能符合的四种是:2003、2323、2643、2963。
思路:做过的最难的一道dp,同学讲给我的思路。
1.首先处理乘积为0的情况。这个相对比较简单,只需用分“有已知的0”和“没有已知的0”两种情况讨论,
用组合数学的知识就可以很快计算出来,略过。
2.接下来考虑乘积不为0的情况。(注意此情况下一定不会有已知的0。)
考虑到最终乘积的质因数只有2,3,5,7四种,因此,
用F[i][a][b][c][d]表示:用i个"?"组成2^a*3^b*5^c*7^d的方案数。
在转移方程中从1到9枚举最后一个"?"的取值x,假设x=2^(a_x)*3^(b_x)*5^(c_x)*7^(d_x),有
F[i][a][b][c][d]=sum{F[i-1][a-a_x][b-a_x][c-c_x][d-d_x]}
初值F[0][0][0][0][0]=1,其他都是0。
假设:
前半部分有ml个问号,且所有已知数字的乘积是al,bl,cl,dl;
后半部分有mr个问号,且所有已知数字的乘积是ar,br,cr,dr;
则最终答案为sum{F[ml][a-al][b-bl][c-cl][d-dl]*F[mr][a-ar][b-br][c-cr][d-dr]}+(乘积为0的方案),其中a,b,c,d遍历了所有可能的取值。
时间复杂度:
乘积为0的部分可在O(n)内解决
动态规划部分由于a<=3n,b<=2n,c<=n,d<=n,所以可在O(n^5)内解决
实现难点:
1. 最终答案需要高精度,不过F用long long就可以了。
2. F的空间复杂度较高,可以使用滚动数组或者类似背包问题的那种办法减少空间开销。
写的时候WA和TLE了几次,对着官网的测试数据debug了45个小时才过,够酸爽~~
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 18
#define A N*3+2
#define B N*2+2
#define C N+2
#define D N+2
char str[(N<<1)+5];
int id,n,nl,nr;
long long ml,mr;
int res[(N<<1)+10],len;
long long dp[2][A][B][C][D],dpt[A][B][C][D];
int fac[13][4] = {//fac[1...9]表示前9个数字,fac[11]和fac[12]表示左边(右边)已经填好数字的积
{0,0,0,0},
{0,0,0,0},
{1,0,0,0},
{0,1,0,0},
{2,0,0,0},
{0,0,1,0},
{1,1,0,0},
{0,0,0,1},
{3,0,0,0},
{0,2,0,0}};
int base[4] = {2,3,5,7};
void jinwei(){//处理进位
int i,j;
for(i = 0;i<=nl+nr;i++){
j = res[i]/10;
res[i] %= 10;
res[i+1] += j;
}
len = nl+nr;
while(len >0 && !res[len])
len--;
}
void add(long long a,long long b){//将a*b累加到输出数组,因为a*b可能溢出long long
int i,j;
if(!a || !b)
return;
for(i = 0;a;i++){
long long num = a%10;
long long x = b*num;
for(j = i;x;j++){
res[j] += x%10;
x /= 10;
}
a /= 10;
}
}
long long pow(int x,int y){//实现幂函数,直接用math库的math会丢失精度
long long sum = 1;
while(y--)
sum *= x;
return sum;
}
int zero_case(){
int i,j;
long long t;
if(!ml && !mr){ //如果两边都出现了0,最简单的情形
res[len = nl+nr] = 1;
return 1;
}
else if(ml && mr){ //如果两边都没出现
add(pow(10, nl)-pow(9, nl),pow(10, nr)-pow(9, nr));//这是两边乘积为0的数量
return 3;
}
else{ //只有一边出现0
if(!ml){
t = pow(10, nr) - pow(9, nr);
i = nl;
}else{
t = pow(10, nl) - pow(9, nl);
i = nr;
}
for(j = i;t;j++){
res[j] = t%10;
t /= 10;
}
len = j-1;
return 2;
}
return 0;
}
void change(){ //算出左边(右边)已有的积的幂表示
int i;
for(i = 0;i<4;i++){
while(ml && ml%base[i] == 0){
fac[11][i]++;
ml /= base[i];
}
while(mr && mr%base[i] == 0){
fac[12][i]++;
mr /= base[i];
}
}
}
int check(int a,int b,int c,int d,int i){
return (a-fac[i][0]>=0)&&(b-fac[i][1]>=0)&&(c-fac[i][2]>=0)&&(d-fac[i][3]>=0);
}
int solve(int n){
int i,j,p,q=0,a,b,c,d,need;
memset(dp, 0, sizeof(dp));
dp[0][0][0][0][0] = 1;
memcpy(dpt, dp[0], sizeof(dp[0]));
for(i = 1;i<=n;i++){
q = i&1; //滚动数组
p = !q;
for(d = 0;d<=i;d++)
for(c = 0;c <= i-d;c++)
for(b = 0;(need=(b+1)/2+d+c)<=i;b++)//注意判断条件,算作剪枝,不写则TLE
for(a = 0;(((b&1)?(a-1):a)+2)/3<=i-need;a++)
for(j = 1;j<=9;j++)
if(check(a,b,c,d,j))
dp[q][a][b][c][d] += dp[p][a-fac[j][0]][b-fac[j][1]][c-fac[j][2]][d-fac[j][3]];
if(i == min(nl,nr)) //记录一下
memcpy(dpt, dp[q], sizeof(dp[q]));
memset(dp[p], 0, sizeof(dp[p]));//注意memset的写法
}
return q;
}
void updateres(int id){
int a,b,c,d,need;
for(d = 0;d<=n;d++)
for(c = 0;c <= n-d;c++)
for(b = 0;(need=(b+1)/2+d+c)<=n;b++)
for(a = 0;(((b&1)?(a-1):a)+2)/3<=n-need;a++)
if(check(a, b, c, d, 11) && check(a, b, c, d, 12)){
if(nl <= nr)
add(dpt[a-fac[11][0]][b-fac[11][1]][c-fac[11][2]][d-fac[11][3]],dp[id][a-fac[12][0]][b-fac[12][1]][c-fac[12][2]][d-fac[12][3]]);
else
add(dp[id][a-fac[11][0]][b-fac[11][1]][c-fac[11][2]][d-fac[11][3]],dpt[a-fac[12][0]][b-fac[12][1]][c-fac[12][2]][d-fac[12][3]]);
}
jinwei();
}
int main(){
int i,j;
nl = nr = len = 0;
ml = mr = 1;
memset(res, 0, sizeof(res));
scanf("%d",&n);
scanf("%s",str);
for(i = 0;i<n;i++)
if(str[i] == '?')
nl++;
else
ml *= str[i]-'0';
for(i = n;i<n*2;i++)
if(str[i] == '?')
nr++;
else
mr *= str[i]-'0';
if(nl+nr == 0){ //没有问号的情况
if(ml == mr)
printf("1\n0");
else
printf("0\n1");
return 0;
}
j = zero_case();
if(j == 1){ //两边都出现了数字0
for(i = len;i>=0;i--)
printf("%d",res[i]);
printf("\n0");
return 0;
}
else if(j == 3){ //两边都没出现数字0
change();
id = solve(max(nl,nr));
updateres(id);
}
for(i = len;i>=0;i--)
printf("%d",res[i]);
putchar('\n');
for(i = nl+nr-1;i>=0;i--) //有所有的情况减去符合的情况就是不符合的情况
res[i] = 9-res[i];
res[nl+nr] = 0;
add(1,1);
jinwei();
for(i = len;i>=0;i--)
printf("%d",res[i]);
return 0;
}