HNOI 2002 Kathy函数 题解

       题目传送门
       题目大意: 问1~m中有多少个数满足 f [ i ] = i f[i]=i

题解

仔细观察这个转移方程——

f [ 1 ] = 1 f[1]=1
f [ 3 ] = 3 f[3]=3
f [ 2 n ] = f [ n ] f[2n]=f[n]
f [ 4 n + 1 ] = 2 f [ 2 n + 1 ] f [ n ] f[4n+1]=2f[2n+1]-f[n]
f [ 4 n + 3 ] = 3 f [ 2 n + 1 ] 2 f [ n ] f[4n+3]=3f[2n+1]-2f[n]

       发现这里面含有大量的2和4出现,于是想到了神奇的二进制,这里先给出结论—— f [ i ] f[i] 的值相当于将i的二进制数反转之后得到的二进制数的十进制。
       比如 100 100 的二进制为 1100100 1100100 ,反转去掉前导零后就变成了 10011 10011 ,也就是说 f [ 100 ] = 1001 1 ( 2 ) = 1 9 ( 10 ) f[100]=10011_{(2)}=19_{(10)}
       下面给出十分不严谨接地气的证明。

证明

分开讨论每一个转移方程。

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

显然,2n的二进制只是在n的二进制后面加了个0,它的二进制反转之后与n的二进制反转是一样的,所以他们 f f 值相等。

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

我们设 f [ n ] f[n] 的二进制为 - ,那么 f [ 2 n + 1 ] f[2n+1] 的二进制就是 1 1- 2 f [ 2 n + 1 ] 2f[2n+1] 的二进制就是 1 0 1-0 ,于是再把 2 f [ 2 n + 1 ] f [ n ] 2f[2n+1]-f[n] 列成竖式:
        1 0 1-0
减           -
——————
        1 1   0 0   -

显然 10 10- 就是 4 n + 1 4n+1 的二进制反转,即 f [ 4 n + 1 ] f[4n+1]

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

与(2)同理。具体过程可以自己模拟一下。

证毕


于是现在题目就变成了:在1~m中,有多少个数的二进制反转等于自己。换句话说,就是有多少个数的二进制是回文的。

一看这个m这么大,显然就要用数位dp了(下文都将m作为二进制数)。

f [ i ] f[i] 表示 i i 位的二进制数中有多少个数是回文的(这个 f f 是dp用的数组,上面那个题目中的f下面将不再提到)。

因为一个二进制数的最高位一定是1,所以只要使它的最低位也是1,中间填个回文数即可使这个数是回文数,所以得到方程 f [ i ] = f [ i 2 ] 2 f[i]=f[i-2]*2 ,乘2是因为 f [ i 2 ] f[i-2] 是指长度为i-2的最高位是1的回文数的数量,但是假如只是插个长度为i-2的数到长度为i的二进制数中的话,最高位是0也没有关系,于是这里要乘2。

长度小于m的长度 的 二进制回文数的个数直接利用 f f 统计即可,对于长度与m相同的,dfs一下即可。dfs的步骤详见程序:

#include <cstdio>
#include <cstring>

int n;
int maxx(int x,int y){return x>y?x:y;}
struct node{
    int a[1010],len;
    node(){for(int i=0;i<=1009;i++)a[i]=0;len=1;}
    friend node operator+(const node &a,const node &b)//由于答案是高精度,所以套个高精度板子
    {
        node c;c.len=maxx(a.len,b.len);
        for(int i=1;i<=c.len;i++)
        {
            c.a[i]+=a.a[i]+b.a[i];
            if(c.a[i]>9)
            {
                c.a[i+1]+=c.a[i]/10;
                c.a[i]%=10;
                if(i==c.len)c.len++;
            }
        }
        return c;
    }
};
node f[1010],p[1010];//p[i]=2^i
int a[1010],t=0;
void work()
{
    f[0].a[1]=f[1].a[1]=f[2].a[1]=1;
    for(int i=3;i<=t;i++)
    f[i]=f[i-2]+f[i-2];
    
    p[0].a[1]=1;p[1].a[1]=2;
    for(int i=2;i<=t;i++)
    p[i]=p[i-1]+p[i-1];
}
node yi,ans;
void die(int x){ans=ans+p[(t-(t-x+1)*2)/2+t%2];}
int ka[1010];
//dfs是在尝试构造一个位数与m相同的比m小的回文二进制数
void dfs(int x)//表示dfs到第x位
{
	//构造方法:先枚举前一半,再判断后一半在与前一半回文时,这个数是否大于m
    if(x>t/2)//前一半
    {
        if(t%2==1&&x==t/2+1)//特判一下m是奇数位时,中间的那一位
        {
            if(a[x]==1)ans=ans+yi;//假如在m中这一位是1,那么如果把这一位搞成0,那么后面无论是什么,这个数都一定小于m,于是直接+1
        	dfs(x-1);//这一位与m的这一位相同的情况
            return;
        }
        else
        {
            if(a[x]==1)
            {
                if(x!=t)die(x);//假如m的这一位是1,可以考虑把它搞成0,那么后面就可以乱填数,贡献的回文数数量就是die(x)
                //因为第x~t位都已经确定了,所以后面对应的那几位也已经确定了,die就是负责统计中间可以填多少个回文数
                ka[x]=1;dfs(x-1);//考虑这一位与m的这一位相同的情况
            }
            else ka[x]=0,dfs(x-1);//别无选择,直接向下搜
        }
    }
    else
    {
        for(int i=x;i>=1;i--)//判断后半段是否可以与前半段构成回文并且不大于m
        {
        	int y=t-i+1;
        	if(a[i]>=ka[y])
        	{
        		if(a[i]>ka[y])
        		{
        			ans=ans+yi;
        			return;
        		}
        	}
        	else return;
        }
        ans=ans+yi;
        return;
    }
}
void count()
{
    if(t==1&&a[t]==1)ans.a[1]=1;//注意要特判一下m=1的情况
    else
    {
        for(int i=1;i<t;i++)
        ans=ans+f[i];
        
        dfs(t);
    }
}
void print()
{
    for(int i=ans.len;i>=1;i--)
    printf("%d",ans.a[i]);
}
char s[1010];
int b[1010];
void input()
{
    scanf("%s",s);
    int m=strlen(s);
    for(int i=1;i<=m;i++)
    b[i]=s[m-i]-'0';
    while(m>1||b[1]>0)//将m转为二进制存在a中
    {
        a[++t]=b[1]%2;
        int last=0;
        for(int i=m;i>=1;i--)
        {
            int la=b[i]%2*5;
            b[i]/=2;
            b[i]+=last;
            last=la;
        }
        if(b[m]==0)m--;
    }
    yi.a[1]=1;
}

int main()
{
    input();
    work();
    count();
    print();
}

猜你喜欢

转载自blog.csdn.net/a_forever_dream/article/details/88637468