BZOJ 1879 Bill的挑战 解题报告


1879: [Sdoi2009]Bill的挑战
Time Limit: 4 Sec  Memory Limit: 64 MB
Description
Sheng_bill不仅有惊人的心算能力,还可以轻松地完成各种统计。在昨天的比赛中,你凭借优秀的程序与他打成了平局,这导致Sheng_bill极度的不满。于是他再次挑战你。这次你可不能输!
      
这次,比赛规则是这样的:
      
N个长度相同的字符串(由小写英文字母和’?’组成),S1, S2, …, SN,求与这N个串中的刚好K个串匹配的字符串T的个数(答案模1000003)。
      
若字符串Sx1xN)和T匹配,满足以下条件:
       1. S
x.length = T.length
       2.
对于任意1iSx.length,满足Sx[i]=’?’或者Sx[i]=T[i]
      
其中T只包含小写英文字母。
Input
本题包含多组数据。 
第一行:一个整数T,表示数据的个数。 
对于每组数据: 
第一行:两个整数,NK(含义如题目表述)。 
接下来N行:每行一个字符串。
T ≤ 5N ≤ 15,字符串长度≤ 50
Output
如题
Sample Input
5
3 3
???r???
???????
???????
3 4
???????
?????a?
???????
3 3
???????
?a??j??
????aa?
3 2
a??????
???????
???????
3 2
???????
???a???
????a??
Sample Output
914852
0
0
871234
67018
Solution
BZOJ的图片题让人看起来确实十分不适,于是笔者不辞劳苦将它转成了文字。
      
本题的Description部分描述的不甚清楚,无法理解题意的读者可以试着把“刚好”换成“恰好”,把“满足以下条件”换成“当且仅当”再次读题。
      
看到“恰好”,一个基本的思路是将其换成“至少”。由于N小于等于15,我们考虑使用状态压缩。注意到,与某一字符串S匹配的字符串的数量取决于S’?’的数量。如果想计算出同时与两个字符串S1S2匹配的字符串的数量,我们可以令S1S2合并,记作S1S2。设S= S1S2,显然方案数取决于S’?’的数量。合并操作的实现如下:
string merge(string a, string b)
{
    int len = a.length();
    string ans = "";
    for (int i = 0; i<len; i++)
    {
        if (a[i] == b[i])ans += a[i];
        else if (a[i] == '?')ans += b[i];
        else if (b[i] == '?')ans += a[i];
        else return "";
    }
    return ans;
}
这里用空字符串表示合并失败。
我们设strdp[i]表示状态i所表示的字符串合并后的结果,由于string对象不便于用memset快速、批量初始化。我们引入辅助变量bitdp[i]bitdp[i]每一位描述strdp[i]在这一位上的字符,其中’?’1表示,小写字母用0表示,比如”abc??ad?”0011000表示(注意:字符串的高位在右侧,数字二进制的高位在左侧)。当strdp[i]=””时,bitdp[i]=-1,这样规定完全是为了用memset初始化。
count1(x)表示x的二进制中1的个数,dp[i]表示与strdp[i]匹配的字符串的数量,显然,我们有dp[i]=26count1(bitdp[i])mod1000003。我们可以用搜索或动态规划求出dpstrdpbitdp三个数组。这里给出用动态规划求解的实现:
//bitdp[0]初始化为len1
bitdp[0] = (1LL << len) - 1;
strdp[0] = "";
//strdp[0]初始化为len'?'
for (int i = 0; i<len; i++)strdp[0] += '?';
for (int i = 0; i<(1 << n); i++)
{
    //如果当前状态可达
    if (bitdp[i] != -1)
    {
        //pow26[i]表示pow(26,i)%mod
        dp[i] = pow26[count1(bitdp[i])];
        for (int j = 0; j<n; j++)
        {
            //状态i的第j位是0,即状态i不包含字符串j
            if (!(i&(1 << j)))
            {
                //尝试将当前状态与字符串j合并
                strdp[i | (1 << j)] = merge(strdp[i], str[j]);
                //bitstrstr的二进制表示,方式与bitdp一样
                bitdp[i | (1 << j)] = bitdp[i] & bitstr[j];
                //合并失败
                if (strdp[i | (1 << j)] == "")
                {
                   bitdp[i | (1 << j)] = -1;
                }
            }
        }
    }
}
求解的过程比较容易想到,但dp数组中存放的不是最终答案。为了保证恰好,我们需要排除匹配了不只k个字符串的情况。我们可是枚举count1(i)=k的所有状态,对于每一个状态,我们枚举将strdp[i]看作一个整体,继续枚举其他字符串,用“奇加偶减”的方式去重。实现如下:
for (int i = 0; i<(1 << n); i++)
{
    if (count1(i) == k)
    {
        int tmp = 0;
        for (int j = 0; j<(1 << n); j++)
        {
            if ((j^i) == (j - i))
            {
                if ((count1(j) - count1(i) + 1) & 1)
                {
                   tmp = (tmp + dp[j]) % mod;
                }
                else
                {
                   tmp = (tmp - dp[j] + mod) % mod;
                }
            }
        }
        ans = (ans + tmp) % mod;
    }
}
这段程序已经有些晦涩了。我们举例说明:
5 3
????ab
ab????
??ew??
???w??
????ab
我们令i=11001,即选择的字符串为”????ab””???w??””????ab”strdp[i]=”???wab”,剩余的两个字符串为”ab????””??ew??”。我们首先试着笔算结果,将strdp[i]看作”??/?/wab”,前两个问号可以用除了ab外的任何一种方式填充,也就是有26×26-1中方式填充,后一个问号不能是e,也就是有26-1中填充方式。这样恰好”????ab””???w??””????ab”这三个字符串匹配的字符串的数量为(26×26-1)×(26-1)=16857。或者用另一种方式计算:dp[11001]-dp[11101]-dp[11011]+dp[11111],也就是与”????ab””???w??””????ab”匹配的方案数减去除了”????ab””???w??””????ab”匹配还与另一个字符串匹配的方案数,再加上重复的减去的方案数。用韦恩图表示如下:




(j^i) == (j - i)一行便是判断状态j是否为状态i的子集,从二进制的角度看,就是i中的每个1,在j中对于的位置也是1
如果BZOJ的测评机不是土豆,这道题将就此结束。但可惜提交的结果是:
我们分析一下复杂度:最外层循环的次数为2n但只有count1(i)==k的状态才会进入第一个if语句,最坏情况为C(15,7)=6435,而对于内层循环,循环次数为2n,最坏情况为215=32768,这里姑且假设count1的用时为比较小的常数,最坏情况程序的循环次数为210862080,显然会TLE
观察重复的计算:每一个状态j都会被使用多次,而可以使用状态j的状态i满足(j^i) == (j - i),也就是说,从j1中选出k个,将其余的1变成0,得到的新状态一定会使用状态j。这个数量为C(count1(j),count1(i))=C(count1(j),k)。于是统计答案的过程变成了:
for (int i = 0; i<(1 << n); i++)
{
    int tmp = count1(i);
    if (tmp >= k)
    {
         if ((tmp - k + 1) & 1)
         {
             ans = (ans + c[tmp][k] * dp[i] % mod) % mod;
         }
         else
         {
             ans = (ans - c[tmp][k] * dp[i] % mod + mod) % mod;
         }
    }
}
注意到,tmp的含义发生了改变,现在它用于储存count1(i),以进一步减少重复计算。
这道题其实还可以进一步优化,我们可以引入dp2,令dp2[i]=Σdp[j], where count1(j) = i。这样可以进一步减少统计答案时的循环次数,在BZOJ 3622的解题报告中读者可以看到这种优化。

Code
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <string>
using namespace std;

inline void read(int &x)
{
    x = 0; char ch = getchar(); bool f = 0;
    while (ch>'9' || ch<'0') { if (ch == '-')f = 1; ch = getchar(); }
    while (ch >= '0'&&ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); }
    if (f)x = -x;
}

inline void read(long long &x)
{
    x = 0; char ch = getchar(); bool f = 0;
    while (ch>'9' || ch<'0') { if (ch == '-')f = 1; ch = getchar(); }
    while (ch >= '0'&&ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); }
    if (f)x = -x;
}

string str[15];
int n, k, len;
//dp[i]——与用i表示的字符串都匹配的方案数
long long dp[32768];
//用状压表示字符串,0——a~z1——?
long long bitstr[15];
long long bitdp[32768];
long long c[16][16];
string strdp[32768];
long long pow26[51];
long long ans = 0;
const long long mod = 1000003;

string merge(string a, string b)
{
    int len = a.length();
    string ans = "";
    for (int i = 0; i<len; i++)
    {
        if (a[i] == b[i])ans += a[i];
        else if (a[i] == '?')ans += b[i];
        else if (b[i] == '?')ans += a[i];
        else return "";
    }
    return ans;
}

int count1(long long x)
{
    int ans = 0;
    while (x) { if (x & 1)ans++; x >>= 1; }
    return ans;
}

int count1(int x)
{
    int ans = 0;
    while (x) { if (x & 1)ans++; x >>= 1; }
    return ans;
}

int main()
{
    int T;
    read(T);
    pow26[0] = 1;
    for (int i = 1; i <= 50; i++)pow26[i] = pow26[i - 1] * 26 % mod;
    c[0][0] = 1;
    for (int i = 1; i <= 15; i++)
    {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++)
        {
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]);
        }
    }
    while (T--)
    {
        ans = 0;
        memset(bitstr, 0, sizeof(bitstr));
        memset(dp, 0, sizeof(dp));
        memset(bitdp, 0xFF, sizeof(bitdp));
        read(n); read(k);
        if (k>n || k<0)
        {
            puts("0");
            continue;
        }
        for (int i = 0; i<n; i++)
        {
            cin >> str[i];
        }
        len = str[0].length();
        for (int i = 0; i<n; i++)
        {
            for (int j = 0; j<len; j++)
            {
                if (str[i][j] == '?')bitstr[i] |= (1LL << j);
            }
        }
        bitdp[0] = (1LL << len) - 1;
        strdp[0] = "";
        //strdp[0]初始化为len'?'
        for (int i = 0; i<len; i++)strdp[0] += '?';
        for (int i = 0; i<(1 << n); i++)
        {
            //如果当前状态可达
            if (bitdp[i] != -1)
            {
                //pow26[i]表示pow(26,i)%mod
                dp[i] = pow26[count1(bitdp[i])];
                for (int j = 0; j<n; j++)
                {
                   //状态i的第j位是0,即状态i不包含字符串j
                   if (!(i&(1 << j)))
                   {
                       //尝试将当前状态与字符串j合并
                       strdp[i | (1 << j)] = merge(strdp[i], str[j]);
                       //bitstrstr的二进制表示,方式与bitdp一样
                       bitdp[i | (1 << j)] = bitdp[i] & bitstr[j];
                       //合并失败
                       if (strdp[i | (1 << j)] == "")
                       {
                           bitdp[i | (1 << j)] = -1;
                       }
                   }
                }
            }
        }
        for (int i = 0; i<(1 << n); i++)
        {
            int tmp = count1(i);
            if (tmp >= k)
            {
                if ((tmp - k + 1) & 1)
                {
                   ans = (ans + c[tmp][k] * dp[i] % mod) % mod;
                }
                else
                {
                   ans = (ans - c[tmp][k] * dp[i] % mod + mod) % mod;
                }
            }
        }
        printf("%lld\n", ans);
    }
    return 0;
}

没有评论:

发表评论