[考试反思]0425省选模拟80:约束
这套题就很难受。
开考不到一小时发现:这套题每道题的所有部分分我都会写,然而每道题的最后一档的分都特别高。
于是暴力写满是$53+47+69=169$。不低。但是既然我能很快想到,那么这个分应该也不是高分。
所以去在弄完$T3$的暴力之后就去尝试弄正解了。
($T3$直接放弃正解,因为首先$69$不低,其次一看这$O(n^2)dp$下一档是$nlogn \times A$这样的,不是矩阵快速幂就是生成函数)
(好家伙,结果两个都是,得亏没做)
莫名其妙把$T1$的正解搞下来挂对拍拍上了,然后寻思着$100+47+69=216$应该可以苟一苟。
于是好像在潜意识里就让脑子停机了。
发现$216$分苟不进前$3$,而且我$T2$的暴力还挂了,把第一档带着第三档一起丢了。没上$200$
我前面的大神们都切了两道题。。。
好像不应该给自己划定约束的。。。最近写了多少个根号了。。。$T2$要是多花点时间应该是可以想出来的吧。。。
根号果然是我的痛处。到现在为止在考场上还没有想到过任何一个根号正解。。。
T1:数字
大意:求满足$L_x \le x \le R_x,L_y \le y \le R_y,x \ or\ y = T$时$x \ and\ y$本质不同的取值有多少种。$T,L_x,L_y,R_x,R_y \le 2^{60}$
非常明显这肯定是数位$dp$。但是仔细想想会遇到一些问题。
部分分:
首先从头来考虑。我们当然要从高到低位来填。
先考虑一个$L_x=L_y=0$的部分分:
大致构造一个$dp$的话就是$dp[i][0/1][0/1]$表示目前考虑了第$i$到更高位,然后是否卡着$R_x,R_y$的上界。
如果或值上这一位是$0$,那么这一位$x,y$都必须是$0$,没啥好说的。
否则分情况讨论,根据卡上界的情况讨论:
如果两个数在这位都只能填$0$,非法。
如果恰好有一个数可以填$0$,那就填,决策也是唯一的。
否则,两个数都能填$0$或$1$,首先两个数都填$1$的话,这一位与出来也是$1$可以继续往下做,
如果一个填$1$一个填$0$的话,这里就有两种填法了。
然而画图或者分类讨论发现,我们填$0$的那个数,以后上界就不卡着了。
也就是说如果现在上界分别是$a,b$,这一位之后会变成$a,\infty$或$\infty,b$
如果$a>b$那么显然前者更优,上界越大能表示出的就越多,会完全包含另一个,所以其实没有分叉,转移下去就好了。
题解做法:
说是$dp$套$dp$,原来是状压套状压啊。(为啥我啥都没听说过啊
如果没有$L_x=L_y=0$的限制。首先状压又多了两个0/1$维。
其次那个要分叉的地方就真的会分叉了。
怎么办呢?我们发现,后续状态只与上下界是否被卡有关。是个$2^4$的状态。
然后,如果我们知道到当前点可以取到哪些状态,我们就知道它能转移到哪些状态。
所以我们把那$2^4$种状态作为权值再进行一次状压,表示每种状态是否可能被取到,这是一个$2^{2^4}$的二进制数。
然后就可以$dp$了。好像细节会很多。时间复杂度$O(65536log)$
恭维一下$LNC$大神奇形怪状的美丽代码。
1 //仓鼠dp选讲的原题,dp套dp 2 #include<bits/stdc++.h> 3 using namespace std; 4 typedef long long ll; 5 ll ans,T,Lx,Rx,Ly,Ry,bin[20],dp[61][1<<15|1]; 6 int get(ll S,int p) {return S>>p&1;} 7 int S(int x,int y,int z,int w) {return x*bin[0]+y*bin[1]+z*bin[2]+w*bin[3];} 8 int main() 9 { 10 scanf("%lld%lld%lld%lld%lld",&T,&Lx,&Rx,&Ly,&Ry); 11 for(int i=bin[0]=1;i<=16;++i) bin[i]=bin[i-1]<<1; 12 dp[60][bin[S(1,1,1,1)]]=1; 13 const int mx=1<<15; 14 for(int w=60,Tw,Lxw,Rxw,Lyw,Ryw;w;--w) 15 { 16 Tw=get(T,w-1);Lxw=get(Lx,w-1);Rxw=get(Rx,w-1);Lyw=get(Ly,w-1);Ryw=get(Ry,w-1); 17 for(int s=1;s<=mx;++s) 18 if(dp[w][s]) 19 for(int Vw=0,t=0;Vw<=1;++Vw,dp[w-1][t]+=dp[w][s],t=0) 20 for(int x=0;x<=1;++x) 21 for(int y=0;y<=1;++y) 22 if((x|y)==Tw&&(x&y)==Vw) 23 for(int i=0;i<=1;++i) 24 if(!(i&&x<Lxw)) 25 for(int j=0;j<=1;++j) 26 if(!(j&&x>Rxw)) 27 for(int k=0;k<=1;++k) 28 if(!(k&&y<Lyw)) 29 for(int l=0;l<=1;++l) 30 if(!(l&&y>Ryw)) 31 if(s&bin[S(i,j,k,l)]) 32 t|=bin[S(i&(x==Lxw),j&(x==Rxw),k&(y==Lyw),l&(y==Ryw))]; 33 } 34 for(int s=1;s<=mx;++s) ans+=dp[0][s]; 35 printf("%lld\n",ans); 36 return 0; 37 }
我的做法:
然而我比较蠢所以想不到这么复杂的东西。
你$dp$有俩出边你不知道走哪个?那你就走比较大的那一个呗。
哪个大你不知道?倒着做$dp$做回来呗,取个$max$就行了啊。
然后我就莫名其妙的写完了。
接下来考虑这个做法的正确性:问题在于,要证明两条出边所到达的构成数字的集合,一定是包含关系,所以有一条路完全没必要走。
首先,考虑当前的限制条件$L_x,L_y,R_x,R_y$(均只保留后几位)
下面定义的包含关系是严格包含,满足$L_x < L_y \le R_y < R_x$。注意两侧不取等,否则当做普通相交处理。
如果两个区间不是包含关系,那么决策是显然的:
因为你现在要给一个数填$0$一个数填$1$,填$0$的那个数会脱离上界限制,另一个脱离下界。
如果两个区间不包含,那么一定一个在上一个在下,那么靠上的解放下界,靠下的解放上界,接下剩余的值域区间一定完全包含另一种决策的。
如果两个区间是包含关系那就这么考虑:
既然两个数都既能填$0/1$,那么就说明,两个数的值域区间都一定包括了$0111..111,1000...000$,也就是说,一定跨过了$1/0$的分界线。
当前的值域区间是$2^i$以内的,到了下一位就一定会减半,具体保留哪一半,与$T$的第$i-1$位是否有值有关。
注意在这里,因为一个数扩充了上界一个数扩充了下界,所以在截取任意一半之后,两个区间一定不再是包含关系。
如果下一位是$0$,也就是值域区间锁定在较低位,两个都填$0$,这里肯定是下界较紧的数扩充下界比较好,能完全包含另一种决策。
如果下一位是$1$,那么如果有小于等于一个数可以填$1$那么转移唯一,两填法等价,
如果两个都可以$01$那么因为上面说了不再是包含关系,所以相交的话一种填法会包含另一种。
综综综综上,分叉的两种转移到的数集是一个包含另一个。得证。
时间复杂度$O(16log)$。至少代码好写就是了。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define ll long long 4 ll T,lx,ly,rx,ry,dp[64][2][2][2][2],ans; 5 int main(){ 6 cin>>T>>lx>>rx>>ly>>ry; 7 for(int l0=0;l0<2;++l0)for(int r0=0;r0<2;++r0)for(int l1=0;l1<2;++l1)for(int r1=0;r1<2;++r1) 8 dp[0][l0][r0][l1][r1]=1; 9 for(int i=0;i<=60;++i){ 10 int Rx=rx>>i&1,Lx=lx>>i&1,Ry=ry>>i&1,Ly=ly>>i&1; 11 for(int l0=0;l0<2;++l0)for(int r0=0;r0<2;++r0)for(int l1=0;l1<2;++l1)for(int r1=0;r1<2;++r1){ 12 int cx0=(!Lx)||l0,cx1=Rx||r0,cy0=(!Ly)||l1,cy1=Ry||r1;ll&D=dp[i+1][l0][r0][l1][r1]; 13 if(T>>i&1){ 14 if(cx1&&cy1)D=dp[i][cx0][r0][cy0][r1]; 15 D+=max(cx1&&cy0?dp[i][cx0][r0][l1][cy1]:0ll,cx0&&cy1?dp[i][l0][cx1][cy0][r1]:0ll); 16 }else if(cx0&&cy0)D+=dp[i][l0][cx1][l1][cy1]; 17 } 18 }cout<<dp[61][0][0][0][0]<<endl; 19 }
T2:跳蚤
大意:支持插入某个数对$(x_i,t_i)$,或者使所有的$x_i+=t_i$,或者给出$L,R$询问$x_i \in [L,R]$的有多少。$1 \le q,x_i,t_i,L,R \le 10^5$
如果$t$很大,那么这些数很快就会跳出询问范围,可以暴力模拟。
然后建一个基于$x$的树状数组,每跳一次就是一个一加一减,查询区间和。可以做到$O(q \frac{max(R)}{t} log\ max(R))$
如果所有$t$都相同,那么可以维护一个动态开点线段树,用那个$tag$的技巧。
每次$x_i+=t_i$操作是全体值加一个相同值,所以只要平移询问区间就可以了,用一个线段树维护就可以。
查询的时候查询所有的线段树就可以。
这样的时间复杂度是$O(qlog)$
这两档部分分都想到了还想不出正解的我大概是个弱智。。
根号分治呗。对于大的$t$还是暴力模拟,对于较小的就对于每一个$t$都开动态开点线段树。总时间复杂度是$O(q\sqrt{10^5} log\ q)$
有一些小的$trick:$
如果不想动态开点,可以考虑这个骚操作:把所有的下标都直接模$10^5$。当接受到一个询问$[l,r]$的时候
就把所有插入时权值($x_p -tag_{that\ time}$)大于$l-tag$的点都删掉,它们显然都非法了,剩下的点要么真的在$[l,r]$内,要么不会对询问产生影响。
在每个线段树的地方配套搞一个堆来维护插入时的权值,如果超出范围就弹堆并从线段树里删除就好。
另一个就是:发现对于$t>\sqrt{10^5}$的部分,有$n\sqrt{n}$次修改$n$次查询,可以$O(2)$修改$O(\sqrt{n})$查询来平摊复杂度。(维护每个块,每个单点内出现了多少值)
对于$t<\sqrt{n}$的部分有$n$次插入$n\sqrt{n}$次查询,可以用(维护前$i$个块一共出现了多少值,在某个块里的前$i$个位置一共有多少值)做到$O(2)$查询$O(\sqrt{n})$修改
可以将总复杂度下降到$O(n\sqrt{n})$。然而好像没必要,并没有写。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define S 100001 4 struct Seg{ 5 int tag,w[S<<2]; 6 priority_queue<int>q; 7 #define lc p<<1 8 #define rc lc|1 9 #define md (L+R>>1) 10 int F(int x){return (x%S+S)%S;} 11 int ask(int l,int r,int p=1,int L=0,int R=S-1){ 12 if(p==1&&l>r)return w[1]-(l-r>1?ask(r+1,l-1):0); 13 if(l<=L&&R<=r||!w[p])return w[p]; 14 return (l<=md?ask(l,r,lc,L,md):0)+(r>md?ask(l,r,rc,md+1,R):0); 15 } 16 void add(int x,int v,int p=1,int L=0,int R=S-1){ 17 w[p]+=v;if(L==R)return; 18 if(x<=md)add(x,v,lc,L,md);else add(x,v,rc,md+1,R); 19 } 20 void ins(int z){ 21 q.push(z-=tag); 22 while(q.top()>=z+S-1)add(F(q.top()),-1),q.pop(); 23 add(F(z),1); 24 } 25 int Ask(int l,int r){ 26 l-=tag;r-=tag; 27 while(q.size()&&q.top()>=l+S-1)add(F(q.top()),-1),q.pop(); 28 return ask(F(l),F(r)); 29 } 30 }T[167]; 31 multiset<pair<int,int>>s,r; 32 int A[S],q; 33 void add(int p,int v){for(;p<S;p+=p&-p)A[p]+=v;} 34 int ask(int p,int a=0){for(;p;p^=p&-p)a+=A[p];return a;} 35 int main(){cin>>q;while(q--){ 36 int op;scanf("%d",&op); 37 if(op==1){ 38 int x,t;scanf("%d%d",&x,&t); 39 if(t>166)s.insert(make_pair(x,t)),add(x,1); 40 else T[t].ins(x); 41 }else if(op==2){ 42 for(int i=1;i<=166;++i)T[i].tag+=i; 43 for(auto x:s){ 44 add(x.first,-1); int z=x.first+x.second; 45 if(z<S)add(z,1),r.insert(make_pair(z,x.second)); 46 }s.clear();swap(s,r); 47 }else{ 48 int l,r,ans=0;scanf("%d%d",&l,&r); 49 for(int i=1;i<=166;++i)ans+=T[i].Ask(l,r); 50 printf("%d\n",ans+ask(r)-ask(l-1)); 51 } 52 }}
T3:棋盘
大意:$n \times m$网格图选择一个格子集合,求联通块数为$k$的方案数。$n \le 3,2^n \times m \le 2 \times 10^5$
先考虑一个暴力$dp$。因为$n$很小,所以我们可以依次考虑每一列的状态。
如果$n \le 2$那都好说。如果$n=3$的话有$9$而不是$8$种状态:$101$这种状态需要考虑这两个$1$是否在一个联通块中。
于是设$dp[i][j][2/4/9]$表示考虑前$i$列$j$个联通块状态如何的方案数。答案就是$dp[m+1][k][0]$。
直接做复杂度是$O(81mk)$的。最终肯定是要搞什么$log$的飞机的。
我们构造生成函数$F(i,s)=\sum\limits_{j} dp[i][j][s] x^j$。然后我们发现并找不到好用的卷积形式。
留得一手$DFT/IDFT$不知道怎么用?为什么不只考虑其中一个呢?
平时做$DFT$的目的是为了得到点值然后快速进行多项式乘法。
可以说点值就是构建了多项式之间的桥梁,让我们从已知多项式走到未知多项式。
然而这道题我们没有已知多项式,却没发现自己已经在桥上了。
对于一个特定的$x$我们能否快速求出其点值?答案是肯定的。
从含义上理解就是:对于一个有$k$个联通块的方案,其贡献为$x^k$,求所有方案贡献和。
这个东西就可以非常简单的矩阵乘法得到了。矩阵的转移系数是关于$x$的低次多项式。
对于特定的$x$值我们可以得到确定的系数矩阵(和多项式无关)做一个非常经典的矩阵快速幂优化$dp$
这样求一个点值是$O(9^3 log \ m)$的
我们只要弄出单位根$w_L$的$0,1,2...L-1$次处的点值然后直接$NTT$给它$IDFT$回去就得到了原多项式的系数。
这里是$O(LlogL)$的。其中$L$是$>\frac{nm}{2}$的最小的$2$的整次幂。
另外一个小的优化是,$100/001,110/011$都是完全相同的状态可以压在一起。常数优化了。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define mod 998244353 4 #define Z 1<<20 5 int n,m,L=1,k,rev[Z],ans[Z],B[8][8],A[8][8];long long C[8][8]; 6 const int S[4]={0,2,3,7}; 7 int qp(int b,int t,int a=1){for(;t;t>>=1,b=1ll*b*b%mod)if(t&1)a=1ll*a*b%mod;return a;} 8 int mo(int x){return x>=mod?x-mod:x;} 9 void IDFT(int*a){ 10 for(int i=1;i<L;++i)if(rev[i]>i)swap(a[i],a[rev[i]]); 11 for(int i=1;i<L;i<<=1)for(int j=0,w=qp(3,mod-1-(mod-1)/2/i);j<L;j+=i<<1) 12 for(int k=j,t=1,x,y;k<j+i;++k,t=1ll*t*w%mod) 13 x=a[k],y=a[k+i]*1ll*t%mod,a[k]=mo(x+y),a[k+i]=mo(x-y+mod); 14 for(int i=0,iv=qp(L,mod-2);i<L;++i)a[i]=1ll*a[i]*iv%mod; 15 } 16 void rebuild(int x){switch(n){ 17 case 1:B[1][1]=B[0][1]=B[0][0]=1; B[1][0]=x; break; 18 case 2:B[2][2]=B[0][2]=B[0][0]=B[1][2]=1; B[2][0]=B[1][0]=x; B[0][1]=B[2][1]=2; B[1][1]=1+x; break; 19 case 3: 20 B[0][0]=B[0][2]=B[0][5]=B[0][6]=B[1][5]=B[1][6]=B[2][2]=B[2][6]=B[3][2]=B[3][5]=B[3][6]=B[4][4]=B[4][6]=B[5][5]=B[5][6]=B[6][2]=B[6][4]=B[6][6]=1; 21 B[0][1]=B[0][3]=B[2][3]=B[3][3]=B[4][1]=B[4][3]=B[6][1]=B[6][3]=2; 22 B[1][0]=B[1][2]=B[2][0]=B[2][5]=B[3][0]=B[4][0]=B[4][2]=B[6][0]=x; 23 B[1][1]=B[1][3]=B[3][1]=x+1; 24 B[2][1]=B[5][1]=B[5][3]=x+x; 25 B[5][0]=B[5][2]=1ll*x*x%mod; 26 }} 27 void mul(int(*a)[8],int(*b)[8]){ 28 for(int i=0;i<=S[n];++i)for(int j=0;j<=S[n];++j)for(int k=0;k<=S[n];++k)C[i][j]+=1ll*a[i][k]*b[k][j]; 29 for(int i=0;i<=S[n];++i)for(int j=0;j<=S[n];++j)a[i][j]=C[i][j]%mod,C[i][j]=0; 30 } 31 int getval(int x){ 32 memset(A,0,256);memset(B,0,256);rebuild(x);A[0][0]=1; 33 for(int t=m;t;t>>=1,mul(B,B))if(t&1)mul(A,B); 34 return A[0][0]; 35 } 36 int main(){ 37 cin>>n>>m>>k; m++; 38 while(L<=n*m/2)L<<=1; 39 for(int i=1;i<L;++i)rev[i]=rev[i>>1]>>1|(i&1?L>>1:0); 40 for(int i=0;i<L;++i)ans[i]=getval(qp(3,(mod-1)/L*i)); 41 IDFT(ans);cout<<ans[k]<<endl; 42 }