[考试反思]0409省选模拟66:大顺
原来大顺指的是暴力能拿高分啊
$T1$的$3^n$过了$19$。$T2$的$2^n n^2$过了$22$。$T3$听他们说$n^3logn$过了$2000$
(虽说我复杂度和他们是一样的但我就没过去。。。)
$T1$是原题强化,然而并不会子集卷积,打了个暴力跑路。
$T2$大概猜到了状态数很少但是并不知道原来搜索真的可以过,写了个状压(数组开大)多拿了$5$分。
$T3$的想法其实大概擦到了$60pts$的边但并没有想下去。暴力跑路。
没想到这次三暴力并不差。
T1:有限空间跳跃理论
大意:图边定向后是$DAG$的方案数。$n \le 20$
被$LNC$疯狂嘲讽。
$dp_{i|j}=\sum\limits_{i \cap j = 0}dp_i \times rate_j$
$rate_j=[j内部无边](-1)^{popcnt(j)-1}$
子集卷积并卡常。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define mod 1000000007 4 int n,m,e[22],dp[23][1<<21],bit[1<<21],ok[22][1<<21]; 5 int add(int&a,int b){a+=b;if(a>=mod)a-=mod;} 6 void FWT(int*a){for(int i=0;i<n;++i)for(int j=0;j<1<<n;++j)if(j&1<<i)add(a[j],a[j^1<<i]);} 7 void IFWT(int*a){for(int i=0;i<n;++i)for(int j=0;j<1<<n;++j)if(j&1<<i)add(a[j],mod-a[j^1<<i]);} 8 int main(){ 9 cin>>n>>m; 10 for(int a,b,i=1;i<=m;++i)scanf("%d%d",&a,&b),e[a-1]|=1<<b-1,e[b-1]|=1<<a-1; 11 for(int i=1;i<1<<n;++i)bit[i]=bit[i^i&-i]+1; 12 for(int i=1;i<1<<n;++i){ 13 ok[bit[i]][i]=bit[i]&1?1:mod-1; 14 for(int j=0;j<n;++j)if(i>>j&1&&e[j]&i)ok[bit[i]][i]=0; 15 } 16 for(int i=1;i<=n;++i)FWT(ok[i]); 17 dp[0][0]=1; FWT(dp[0]); 18 for(int i=0;i<n;++i)for(int j=1;i+j<=n;++j)for(int x=0;x<1<<n;++x)if(dp[i][x]&&ok[j][x]) 19 dp[i+j][x]=(dp[i+j][x]+1ll*dp[i][x]*ok[j][x])%mod; 20 IFWT(dp[n]); 21 cout<<dp[n][(1<<n)-1]; 22 }
T2:神秘代码
大意:给定$01$串$s$求有多少排列$a$满足$s_i=1$则$a_i=2a_{i-1}$或$a_{i-1}=2a_i$。若$s_i=0$则满足$a_i\neq 2a_{i-1}$且$a_{i-1}\neq 2a_i,n \le 40$
可以发现$2$倍关系构成若干链。然后每次要求取出一段特定长度的链,然后下一个位置上$ban$掉零或一或二种数。
记搜,状态压一下长度为$1,2,3,4,5,6$的链各还有几个以及$ban$掉的链的长度即可。
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define mod 1000000007 4 int cnt[7],nd[66],n,d;char c[66]; 5 struct hash_map{ 6 #define S 235711 7 int fir[S],l[S],v[S],ec,to[S]; 8 int&operator[](int x){int r=x%S; 9 for(int i=fir[r];i;i=l[i])if(to[i]==x)return v[i]; 10 l[++ec]=fir[r];fir[r]=ec;to[ec]=x;return v[ec]=-1; 11 } 12 }M[66]; 13 int sch(int o,int x,int y){//cerr<<o<<' '<<cnt[1]<<' '<<cnt[2]<<' '<<cnt[3]<<endl; 14 if(o==d+1)return 1; 15 int&ans=M[o][cnt[6]<<29|cnt[5]<<26|cnt[4]<<22|cnt[3]<<17|cnt[2]<<12|cnt[1]<<6|x<<3|y],r; 16 #define I i-j-nd[o] 17 if(ans!=-1)return ans; ans=0; 18 if(nd[o]==1){ 19 for(int i=1;i<=6;++i)for(int j=0;j<i;++j)if(r=cnt[i]-(x==i&&!j)-(y==i&&!j)) 20 cnt[i]--,cnt[j]++,cnt[I]++,ans=(ans+1ll*r*sch(o+1,j,I))%mod,cnt[i]++,cnt[j]--,cnt[I]--; 21 }else for(int i=nd[o];i<=6;++i)for(int j=0;j+nd[o]<=i;++j)if(r=cnt[i]) 22 cnt[i]--,cnt[j]++,cnt[I]++,ans=(ans+1ll*r*sch(o+1,j,0)+1ll*(r-(x==i&&!j)-(y==i&&!j))*sch(o+1,I,0))%mod,cnt[i]++,cnt[j]--,cnt[I]--; 23 return ans; 24 } 25 int main(){//freopen("code8.in","r",stdin); 26 cin>>n>>c+1; 27 if(c[1]=='0')nd[d=1]=1; 28 for(int i=1;i<n;++i){ 29 if(nd[d]==1&&c[i]=='1')d--; d++;nd[d]=1; 30 while(c[i]=='1')i++,nd[d]++; 31 if(c[i]=='0'&&nd[d]!=1)nd[++d]=1; 32 }//for(int i=1;i<=d;++i)cerr<<nd[i]<<' ';cerr<<endl; 33 for(int j=n/2+1;j<=n;++j){ 34 int r=j,c=1; 35 while(!(r&1))r>>=1,c++; 36 cnt[c]++; 37 }cout<<sch(1,0,0)<<endl; 38 }
T3:预言
大意:有$n$个区间$l_i,r_i$。要求选定一个排列$P$满足$l_{P_j} \le j \le r_{P_j}$。最小化$P$字典序。无解输出$-1$。$n \le 10^5$
鸽了。牛爷爷怎么什么都会啊。
在颓了好几次牛爷爷的题解之后终于知道是啥意思了。
首先我们设$L_i,R_i$分别表示左/右端点$\le i$的区间有多少个。
那么我们对于有解的判定,显然有一个必要但不充分的条件:$L_i \ge i,R_i \le i$
然后有一个根本想不到的结论:如果你在指定$P_1$并删掉对应区间然后从$i=2$开始计算,如果依然满足上述条件则一定存在合法解。
证明的话,就是说,如果你选定一个区间,那么对于剩下的数再次进行排列:
相当于所有区间的两个端点$-1$,然后同时$i=1$的限制不存在,其余所有$i$都要$-1$。也就是说数组平移那样子。
那么来考虑一下,在你指定一个区间之后,一定是所有的$L_i$都$-1$(左端点一定是合法的),然后所有$i$也要$-1$。所以依旧合法。
对于$R_i$而言稍微麻烦一些:加入存在一组合法解,其中你第一个选择的区间是$[l_t,r_t]$
如果的确存在合法解的话,你尝试用其他的区间$[l_x,r_x]$来替代它作为第一个
那么也就是说,你第一次选择之后,相较于原方案,多出了一个$[l_t,r_t]$少了一个$[l_x,r_x]$
(显然有$l_x=l_t=1$)
对于剩下的位,我们进行贪心算法(只要求有解的贪心)
我们会优先使用右端点最小的区间去填上当前位置右端点大的留着后面用。
那么也就是说,我们开一个变量$y$,初值是$r_t$表示那个多余的区间,然后我们从$i=2$开始遍历序列。
每次遇到一个右端点更大的区间则用它来替代$y$。要求每时每刻$y \ge i$。
一旦出现$y < i$那么也就是说明,有超过$i-1$个区间它们的右端点$\le i$,那么就无解。也就是说$R_i > i$
否则贪心就能执行到最后并找到一组合法解。
所以说,我们只需要维护一棵线段树来存储$i-R_i$的值。
然后逐位确定字典序,我们发现如果我们选定了一个区间$[l,r]$那么影响就是,所有的$i$都$-1$,同时$R_{r+1...n}$也都要减$1$
那么最终的总的影响就是$[i,r]$区间的$i-R_i$值要减一。我们需要满足所有位置的$i-R_i$始终非负。
那么只需要每次线段树二分找到第一个$i-R_i=0$的位置,就说明我们在当前位上可以任意选择一个右端点$\le i$的区间。
于是我们只需要再维护另一个线段树用来查询右端点$\le i$的区间的编号最小的是哪个区间。
然后只需要下标基于区间编号,维护右端点的最小值就也可以线段树二分了。
上面的$i-R_i$是一开始就处理好的,下面的这个线段树是在扫描线一样扫左端点的时候加入的。
总的时间复杂度是$O(n\ log n)$
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define S 500005 4 int n,w[S],ans[S],v[S],lz[S]; 5 vector<int>rp[S],pos[S]; 6 #define lc p<<1 7 #define rc lc|1 8 #define md (L+R>>1) 9 void down(int p){w[lc]+=lz[p];w[rc]+=lz[p];lz[lc]+=lz[p];lz[rc]+=lz[p];lz[p]=0;} 10 void build(int p=1,int L=1,int R=n){ 11 w[p]=L; v[p]=n+1; if(L==R)return; 12 build(lc,L,md); build(rc,md+1,R); 13 } 14 void add(int l,int r,int d,int p=1,int L=1,int R=n){ 15 if(l>r)return; 16 if(l<=L&&R<=r){lz[p]+=d;w[p]+=d;return;} 17 if(lz[p])down(p); 18 if(l<=md)add(l,r,d,lc,L,md); if(r>md)add(l,r,d,rc,md+1,R); 19 w[p]=min(w[lc],w[rc]); 20 } 21 int ask(int p=1,int L=1,int R=n){ 22 if(L==R)return L; 23 if(lz[p])down(p); 24 int z=w[lc]?ask(rc,md+1,R):ask(lc,L,md); w[p]=min(w[lc],w[rc]); return z; 25 } 26 void ins(int P,int z,int p=1,int L=1,int R=n){ 27 if(L==R){v[p]=z;return;} 28 if(P<=md)ins(P,z,lc,L,md); else ins(P,z,rc,md+1,R); 29 v[p]=min(v[lc],v[rc]); 30 } 31 int query(int E,int z,int p=1,int L=1,int R=n){ 32 if(L==R){ 33 if(v[p]==n+1)return puts("-1"),exit(0),0; 34 add(E+1,v[p]-1,-1);add(E,E,n); v[p]=n+1; return L; 35 } 36 int Z=v[lc]<=z?query(E,z,lc,L,md):query(E,z,rc,md+1,R); 37 v[p]=min(v[lc],v[rc]); return Z; 38 } 39 int main(){ 40 cin>>n; 41 build(); 42 for(int i=1,a,b;i<=n;++i)scanf("%d%d",&a,&b),rp[a].push_back(b),pos[a].push_back(i),add(b,n,-1); 43 for(int i=1;i<=n;++i){ 44 for(int j=0;j<rp[i].size();++j)ins(pos[i][j],rp[i][j]); 45 ans[i]=query(i,ask()); 46 } 47 for(int i=1;i<=n;++i)printf("%d ",ans[i]); 48 }