[考试反思]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 }
View Code

 

我的做法:

然而我比较蠢所以想不到这么复杂的东西。

你$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 }
View Code

 

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 }}
View Code

 

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 }
View Code

 

posted @ 2020-04-25 21:29  DeepinC  阅读(234)  评论(0编辑  收藏  举报