zoukankan      html  css  js  c++  java
  • [考试反思]0425省选模拟80:约束

    这套题就很难受。

    开考不到一小时发现:这套题每道题的所有部分分我都会写,然而每道题的最后一档的分都特别高。

    于是暴力写满是$53+47+69=169$。不低。但是既然我能很快想到,那么这个分应该也不是高分。

    所以去在弄完$T3$的暴力之后就去尝试弄正解了。

    ($T3$直接放弃正解,因为首先$69$不低,其次一看这$O(n^2)dp$下一档是$nlogn imes 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
    ",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(qsqrt{10^5} log q)$

    有一些小的$trick:$

    如果不想动态开点,可以考虑这个骚操作:把所有的下标都直接模$10^5$。当接受到一个询问$[l,r]$的时候

    就把所有插入时权值($x_p -tag_{that time}$)大于$l-tag$的点都删掉,它们显然都非法了,剩下的点要么真的在$[l,r]$内,要么不会对询问产生影响。

    在每个线段树的地方配套搞一个堆来维护插入时的权值,如果超出范围就弹堆并从线段树里删除就好。

    另一个就是:发现对于$t>sqrt{10^5}$的部分,有$nsqrt{n}$次修改$n$次查询,可以$O(2)$修改$O(sqrt{n})$查询来平摊复杂度。(维护每个块,每个单点内出现了多少值)

    对于$t<sqrt{n}$的部分有$n$次插入$nsqrt{n}$次查询,可以用(维护前$i$个块一共出现了多少值,在某个块里的前$i$个位置一共有多少值)做到$O(2)$查询$O(sqrt{n})$修改

    可以将总复杂度下降到$O(nsqrt{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
    ",ans+ask(r)-ask(l-1));
    51     }
    52 }}
    View Code

    T3:棋盘

    大意:$n imes m$网格图选择一个格子集合,求联通块数为$k$的方案数。$n le 3,2^n imes m le 2 imes 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)=sumlimits_{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
  • 相关阅读:
    C#中datatabel导出excel(三种方法)
    JDBC 使用说明
    c# lock (obj) 与 lock (this) 区别
    步步深入MySQL:架构>查询执行流程>SQL解析顺序
    SqlParameter的作用与用法
    c# winform窗口自适应各种分辨率类
    SQL中一个很好用的日期格式化函数
    C#生成缩略图
    设置VMware随系统开机自动启动并引导虚拟机操作系统
    在编写PL/SQL代码中使用SELECT语句时如何避免例外发生
  • 原文地址:https://www.cnblogs.com/hzoi-DeepinC/p/12775384.html
Copyright © 2011-2022 走看看