1. 程式人生 > >poj 1608 dp(Banal Ticket)

poj 1608 dp(Banal Ticket)

題意:給定一個長度為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;
}