查看文章
 
2008年成都现场赛H题“Toy”解题报告
2009-06-02 18:25

       原题可以在HDU上找到:http://acm.hdu.edu.cn/showproblem.php?pid=2481

       诚然,这是一道非常难的题目,当时在现场只有清华的陈丹琦那个队出了这道题,他们是唯一一个出了7道题的队伍。


       去年成都赛的时候,我们都是菜鸟,我对此毫无头绪,只是觉得有点像楼天成Color那个题,但我当时不会用Burnside引理。好在我们队的肖侃同学想到了一个十分强大的公式:2^n-n+1。验证了n=1,2,3,4,均成立,于是想敲这个题,孙楠反对,认为正确的几率太小,唯一的一台电脑应该用来做更有希望的题。但我当时支持肖侃,因为2^n-n+1是一个五分钟内绝对能敲完的东西,我觉得用五分钟拼一拼人品是值得的。在我和肖侃的要求下,我们就让肖侃上了....

        最后的结果是,这道题至少浪费了我们20分钟。因为我们不熟悉高幂取模的写法... ...当时我们那叫一个菜.....算了

        后来我们找清华的队问这道题的解法,那个男生大致说了一下,我当时不懂,所以记住的不多。隐约记得的几个词语是:把N分解因子,枚举公约数,Burnside引理。 其实以我们当时的实力,这题不是我们该问的。

        今年我学习了大量的新知识后,终于于昨天把它A掉了,下面与大家分享一下这一题的想法:

        这一题有两个难点,一个是如何统计不可旋转时,合法的方案总数。方法是先假象一个动态规划算法,然后找出递推公式,再表示成矩阵的幂。

        另一个难点就是通过枚举循环节,利用欧拉函数快速统计所有置换中不变方案的总和。这一点本来是比较难的,但是如果做过楼天成的Color那个题,这个技术就可以依葫芦画瓢了。

        如果你没做过楼天成的color那个题,我建议你先做一下color再来做这道题,这个Toy比color难,color在http://acm.pku.edu.cn/JudgeOnline/problem?id=2154 ,是Polya计数,枚举统计的部分和这题一模一样。唯一的不同时Toy这题的染色是带有限制的,需要另想办法计算确定了循环节之后的方案数,而不是简单的N^k。

        我们来看看假如图形不允许旋转,如何统计不同的方案总数。

       在图上选固定的A、B两个相邻的点。假设AB两个点之间没有边相连,我们设整个图形中有节点数为n(不包括中间点),AB之间没有边相连的方案总数为 f[n]。枚举A往右边数连接的最多的节点数,假设这个数量是k(如图),把这K各节点去掉,由B到C之间可以看成一个新的图形,总共有n-k各节点,BC之间断开(是指沿弧BAC方向,B不与C直接相连),显然这个新图形内部的连接方案总数为f[n-k]。

       再考虑到从A到k这k各节点一定有且只有一个与中心相连,因此本身有k种接法,所以如果A往右边连续连接了k各节点,那么这个连接方案总数是f[n-k]*k。 再枚举k,得到:

      f[n]=f[0]*n+f[1]*(n-1)+f[2]*(n-2)+f[3]*(n-3)+.....+f[n-1]*1.

     规定f[0]=1。因为f[0]*n这一项表示从外围除了AB之间全部连上的情况,恰有n种,所以规定f[0]=1是合理的。

     设s[n]是从f[0]到f[n]的总和,那么上式可以写为:

     f[n]=s[n-1]+f[0]*(n-1)+f[1]*(n-2)+f[2]*(n-3)+....f[n-2]*1

           =s[n-1]+f[n-1]=s[n-2]+2*f[n-1]

          = (f[n-1]-f[n-2])+2f[n-1]

          =3*f[n-1]-f[n-2],

        注意变换的过程中认为s[n-2]=f[n-1]-f[n-2]是因为第二部中已经发现f[n]=s[n-1]+f[n-1]。由于n是指任意自然数,所以这个结论对于f[n-1]也成立。

         到这一步,我们就已经可以通过求矩阵的幂来计算f[n]了,考虑到f[1]=1,f[2]=3,还可以进一步得出f[n]=Fibonacci[2n-1]。

         f[n]是指A、B不相连的连接方案数。考虑A、B相连,同样,我们设与AB连续相连的节点有k个(如图)

           我们设总共有n个节点,AB相连的方案数为g[n]。如图,图中从1到k共k各节点是连续相连的,1与1左边那个不相连,k与k右边那个不相连,AB是这一段边上相邻的两个点。因为AB包含在1...k中,所以这条边的位置有k-1种选择;同样,这条边中有且只有一个与中心相连,从而本身有k种选择;除去1...k剩下的那一部分有n-k各节点,本身有f[n-k]种选择。

           所以,图形中A、B两点相连,并且与A、B连续相连的节点有k个时,排列的方案数为(k-1)*k*f[n-k],枚举k,得到:

           g[n]=(2-1)*2*f[n-2]+(3-1)*3*f[n-3]+......((n-1)-1)*(n-1)*f[1];

          为了得出g[n]与f[n]的关系,计算g[n]-g[n-1]:

           g[n]-g[n-1]=2*f[n-2]+4*f[n-3]+6*f[n-4]+....2*(n-2)*f[1]

                           =2*f[n-1]

           从而:

           g[n]=2*(f[1]+f[2]+f[3]+....f[n-1]).

          f[n]是可以通过计算矩阵的幂得到的,设这个矩阵是A,A={{2,1},{1,1}},计算A+A^2+A^3+.....A^(n-1)就可以得到 f[1]+f[2]+f[3]+....f[n-1] 。如果你不清楚怎样快速计算A+A^2+A^3+.....A^(n-1),去看一下这个题:http://acm.pku.edu.cn/JudgeOnline/problem?id=3233

          现在已经有办法快速计算如果不允许旋转,合法的方案总数了。题目中说了旋转之后算相同的方案,怎么办呢?当然是使用Burnside引理,总共n个置换,统计每个置换下保持不变的方案总数之和,再除以n就可以了。假设置换c[i]表示将整个图形顺时针转过i个节点,那么置换i的循环节总数就是gcd(i,n),每个循环的长度是n/gcd(i,n)。对于循环节总个数是k,是每个循环节都相等的连接方案总数 与 总共有k个节点,连接不受限制的连接方案总数是相同的。因为你可以假想在整个图形中截取一小段长为gcd(i,n),然后把他们首尾相连就构成了一个新的有gcd(i,n)个节点的图形,它的连接方案总数与原来的整个图形把每个循环节都置成相同的连接方案总数是一致的。

          n<=10^9,我们不能逐一统计每个置换的gcd(i,n)值,好在循环节长度为gcd(i,n)的置换总数量,就是(n/gcd(i,n))的欧拉函数。按照这个方法枚举gcd(i,n)就可以快速统计所有置换中保持不变的方案总数之和了。这一部分的具体的细节可以参考POJ2154,就是刚才说了楼天成的color那个题,color到处都有解题报告(但是Toy的解题报告就不好找了,所以我决定写一份)。

         基本思路就是这样的,最后在取模的环境下除N,且不保证MN互素,我的办法是先把一开始就把M乘上N,这样最后的答案就能直接除N了,附上代码:

#include<iostream>
using namespace std;
__int64 pow[40][2][2]={{{2,1},{1,1}}};   //pow[i]表示A^(2^i);
__int64 sum[40][2][2]={{{2,1},{1,1}}};   //sum[i]表示A+A^2+...+A^(2^i);
__int64 M,N;
int facts[100],p;
int fnum[100];
int gcds[10000],g;
__int64 mul_mod(__int64 a,__int64 b)
{
      __int64 res=0;
      while(b!=0)
      {
            if(b&1)
            {
                  res+=a;
                  if(res>=M)
                        res-=M;
            }
            a=a<<1;
            if(a>=M)
                  a-=M;
            b>>=1;
      }
      return res;
}
inline void add(__int64 a[2][2],__int64 b[2][2])
{
      a[0][0]+=b[0][0];
      if(a[0][0]>=M)
            a[0][0]-=M;
      a[0][1]+=b[0][1];
      if(a[0][1]>=M)
            a[0][1]-=M;
      a[1][0]+=b[1][0];
      if(a[1][0]>=M)
            a[1][0]-=M;
      a[1][0]+=b[1][0];
      if(a[1][1]>=M)
            a[1][1]-=M;
}
inline void mov(__int64 a[2][2],__int64 b[2][2])
{
      a[0][0]=b[0][0];
      a[0][1]=b[0][1];
      a[1][0]=b[1][0];
      a[1][0]=b[1][0];
}
void mul(__int64 c[2][2],__int64 a[2][2],__int64 b[2][2])
{
      c[0][0]=mul_mod(a[0][0],b[0][0])+mul_mod(a[0][1],b[1][0]);
      if(c[0][0]>=M)
            c[0][0]-=M;
      c[0][1]=mul_mod(a[0][0],b[0][1])+mul_mod(a[0][1],b[1][1]);
      if(c[0][1]>=M)
            c[0][1]-=M;
      c[1][0]=mul_mod(a[1][0],b[0][0])+mul_mod(a[1][1],b[1][0]);
      if(c[1][0]>=M)
            c[1][0]-=M;
      c[1][1]=mul_mod(a[1][0],b[0][1])+mul_mod(a[1][1],b[1][1]);
      if(c[1][1]>=M)
            c[1][1]-=M;
}
void mat_init()
{
      for(int i=1;i<35;i++)
      {
            mul(pow[i],pow[i-1],pow[i-1]);
            mul(sum[i],sum[i-1],pow[i-1]);
            add(sum[i],sum[i-1]);
      }
}
__int64 fib(int n)
{
      int i=0;
      __int64 tmp[2][2],res[2][2]={{1,0},{0,1}};
      while(n!=0)
      {
            if(n&1)
            {
                  mul(tmp,res,pow[i]);
                  mov(res,tmp);
            }
            n>>=1;
            i++;
      }
      return res[0][1];
}
__int64 sigma_fib(int n)
{
      int i=0;
      __int64 tmp[2][2],res[2][2]={0};
      while(n!=0)
      {
            if(n&1)
            {
                  mul(tmp,res,pow[i]);
                  mov(res,tmp);
                  add(res,sum[i]);
            }
            n>>=1;
            i++;
      }
      return res[0][1];
}
__int64 stable_stat(int n)
{
      __int64 res=fib(n)+2*sigma_fib(n-1);
      res%=M;
      return res;
}
void ana()
{
    p=0;
    int i,tn=N;
    for(i=2;i*i<=tn;i++)
        if(tn%i==0)
        {
            facts[p]=i;
            fnum[p]=0;
            do
            {
                 fnum[p]++;
                 tn/=i;
            }
            while(tn%i==0);
            p++;
        }
    if(tn!=1)
    {
          fnum[p]=1;
          facts[p++]=tn;
    }
}
void allgcds(int dp,int now)
{
      if(dp==p)
            gcds[g++]=now;
      else
            for(int i=0;i<=fnum[dp];i++)
            {
                  allgcds(dp+1,now);
                  now*=facts[dp];
            }
}
__int64 phi(int n)
{
    int i,res=n;
    if(n==1)
       return 1;
    for(i=0;i<p;i++)
    {
        if(n%facts[i]==0)
        {
            res/=facts[i];
            res*=(facts[i]-1);
        }
    }
    return res;
}
__int64 work()
{
    if(N==1)
       return 1;
    __int64 t,res=phi(N)%M;
    int i;
    for(i=1;i<g;i++)
    {
          t=mul_mod(stable_stat(gcds[i]),phi(N/gcds[i]));
          res=(res+t)%M;
    }
    return res/N;
}
int main()
{
      while(scanf("%I64d%I64d",&N,&M)==2)
      {
            M*=N;
            mat_init();
            ana();
            g=0;
            allgcds(0,1);
            printf("%I64d\n",work());
      }
      return 0;
}

         顺便说一下,这份代码在HDU上跑得特别快,A掉此题的另外三个人中有两个分别跑了7000多ms和6000多ms,最快的acrush也跑了5000多ms,而这份代码只要400多ms,快了10倍多。我不知道别人用的什么算法,我觉得统计不旋转的方案总数可能有更简单的算法,我这种分析真是太麻烦了。

        在现场赛中清华它们应该用了更好的办法,它们讲的难点似乎主要是枚举循环节的一部分。如果在时间紧迫的的现场赛,统计不旋转的方案总数用我这种方法,那真是太恐怖了。希望清华他们也发一份解题报告,分享一下他们更为巧妙的方法


类别:acm现场赛 解题报告||添加到搜藏 |分享到i贴吧|浏览(1198)|评论 (0)
 
最近读者:
 
网友评论:
发表评论:
姓 名:
网址或邮箱: (选填)
内 容:
     

   
帮助中心 | 空间客服 | 投诉中心 | 空间协议
©2012 Baidu