BZOJ 4513: [Sdoi2016]储能表(记忆化搜索)

题目

原题链接
格子(i,j)的大小是max(i^j-k,0),求一个前缀矩阵mod p(动态给出)的值,范围可达1e18,5000组询问。

分析

网上搜题解,握草,只写一个状态!?
网上搜代码,握草,全是填表法!?

还有什么叫直接DP就好了,小生完全不会啊啊啊啊啊!!!!

还有这道题好像可以找规律,然后我数位DP分析着分析着就变成找规律了,而且还很疑惑觉得为什么和周围的小伙伴不一样?要注意数位DP的内涵啊。

切入正题:
由于是xor运算,很容易想到转换成二进制来做。

我们实际上要做的就是要找到这样的一些点对的亦或和:i<=n && j<=m && i^j>=k,然后从高位开始考虑,因此记忆化搜索的时候需要同时把n,m,k转换成二进制后带入限制,其中这里k是向下的限制,所以和一般的数位DP略有不同。

在转移的时候,我们考虑的是最高位可以选择的情况,正如一般的数位DP一样,但是我们没有必要并且并不容易把两个元素的同一位分开考虑,所以对于一个状态我们同事考虑两个元素pos的选择。

所以我们在记忆化的时候用f(i,lim1,lim2,lim3)表示当前第i位,n,m,k是否有限制时的前缀矩阵和。

推到后面你发现推不下去了,所以自然而然加上一个表示i^j>=k的元素的个数。就可以顺利递推了。

可是为什么网上这么多人选择直接递推呢?因为不同于一般的数位DP,这里同一个限制状态可能被多个不同的转移调用,不记录有限制的时候的状态会T,所以这次我们要记忆转移的所有状态,包括限制情况。这么一来,所有状态都被记录,没有任何必要用记忆化了,不过这种要记录限制的情况只能对一个数使用,因为不同的数限制不同。

不过我还是写了记忆化来填补一下网上题解没有记忆化的遗憾嘛。

代码

  1. 两个DP可以通过传值变量合成一个,写成两个方便理解,上面是个数计算,下面是和的计算。
  2. 注意每次分解数组要清空。
  3. 有很多爆int的地方要小心。
#include<cmath>
#include<queue>
#include<cstdio>
#include<vector>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int maxn=65;
int T,p;
LL n,m,k;
LL f[maxn][2][2][2],g[maxn][2][2][2];
int z1[maxn],z2[maxn],z3[maxn];
LL dp1(int pos,bool lim1,bool lim2,bool lim3)
{
    if(pos<1)return 1;
    if(g[pos][lim1][lim2][lim3]!=-1)return g[pos][lim1][lim2][lim3];
    int up1=lim1?z1[pos]:1;
    int up2=lim2?z2[pos]:1;
    int down=lim3?z3[pos]:0;
    bool l1,l2,l3;
    LL ret=0;
    for(int a=0;a<=up1;a++)
    {
        for(int b=0;b<=up2;b++)
        {
            if((a^b)<down)continue;
            l1=lim1 && a==up1;
            l2=lim2 && b==up2;
            l3=lim3 && (a^b)==down;
            ret=(ret+dp1(pos-1,l1,l2,l3))%p;
        }
    }
    return g[pos][lim1][lim2][lim3]=ret;
}
LL dp2(int pos,bool lim1,bool lim2,bool lim3)
{
    if(pos<1)return 0;
    if(f[pos][lim1][lim2][lim3]!=-1)return f[pos][lim1][lim2][lim3];;
    int up1=lim1?z1[pos]:1;
    int up2=lim2?z2[pos]:1;
    int down=lim3?z3[pos]:0;
    bool l1,l2,l3;
    LL ret=0;
    for(int a=0;a<=up1;a++)
    {
        for(int b=0;b<=up2;b++)
        {
            if((a^b)<down)continue;
            l1=lim1 && a==up1;
            l2=lim2 && b==up2;
            l3=lim3 && (a^b)==down;
            ret=(ret+dp2(pos-1,l1,l2,l3))%p;
            if(a^b)ret=( ret + ( ( 1ll << ( pos-1 ) ) %p ) * dp1(pos-1,l1,l2,l3) )%p;
        }
    }
    return f[pos][lim1][lim2][lim3]=ret;
}
void fenjie(LL x,int *z)
{
    int sz=0;
    do{
        z[++sz]=x&1;
        x>>=1;
    }while(x);
}
void calc()
{
    scanf("%lld%lld%lld%d",&n,&m,&k,&p);
    n--,m--;
    memset(f,-1,sizeof(f));
    memset(g,-1,sizeof(g));
    memset(z1,0,sizeof(z1));
    memset(z2,0,sizeof(z2));
    memset(z3,0,sizeof(z3));
    fenjie(n,z1);
    fenjie(m,z2);
    fenjie(k,z3);
    printf("%lld\n",(dp2(61,1,1,1)-1ll*k%p*dp1(61,1,1,1)%p+p)%p);
}
int main()
{
    scanf("%d",&T);
    while(T--)
        calc();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值