2021牛客暑期多校训练营3
比赛链接:https://ac.nowcoder.com/acm/contest/11254
很费劲地 3/10 ,但是校内第18名囧。
一开始没什么一眼做出来的题;后来发现J题好多人过,于是G开始写J;但似乎写了一会以后遇到了障碍,停下继续推了起来。而我先看了B,E,F,发现B题渐渐有一些人过了,于是开始看B。此时Y也在看B。一个小时多一点的时候,J过了;Y在看E,我在看B,然后G看F——目前就是B,E,F,J过的人比较多。B应该是 \( O(n^2) \) 做,就开始想DP,但是没什么很好的想法。之后Y发现了E题的规律,写出了个式子,于是我两人就一起讨论怎么写出来;讨论一番,觉得预处理一下,然后二分就能写。此时G发现F题能搜索,已写了半天。然后Y去写E题,很快写完,但遇到了点问题,G也去调了半天,就过了。这时已经四点了。G继续写F题,而Y和我一起看B题;我又写了DP的转移方程,但是怎么看也是 \( O(n^3) \) 的,苦恼。Y在考虑构造,但也没想好。还剩十分钟的时候F题终于过了。于是我上去写了个B题的简单假算法,果然WA了,而且还一个点都没过,真不给面子啊。
题意:
\(t\)组数据,每次给一个\(n\),求满足\( (xy+1) | (x^2+y^2) \)的\( (x,y) \)个数,\(1 \leq x,y \leq n\)。\(1 \leq t \leq 10^5, 1 \leq n \leq 10^18\)。
分析:
通过打表,发现除了\((1,1)\),满足条件的是所有\( (k,k^3) \),以及它们递推下去的数对;
就是说,如果\((a,b)\)满足条件,而且是从\((k,k^3)\)推出来的,那么\( (b,b*k^2-a) \)也是满足条件的数对;
所以找到所有满足条件的数对(也不是很多),每次询问upper_bound即可。
数字比较大,需要用__int128.
Y写了。
#include<bits/stdc++.h> #define ll long long using namespace std; const int MAXN=1e7+10; ll num[MAXN]; int T,cnt; ll ans; void init(){ num[1]=1;cnt=1; __int128 a,b; for(int i=2;i<=1e6;i++){ a=i;b=i,b=b*i*i; while(b<=1e18){ //if(b==2211||b==2424||b==4863) printf("%lld %lld\n",(ll)a,(ll)b); num[++cnt]=(ll)b; ll t=b; b=b*i*i-a;a=t; //printf("i=%d a=%lld b=%lld\n",i,(ll)a,(ll)b); //if(b>1e18||b<0) printf("i=%d b=%lld\n",i,(ll)b),exit(0); } } sort(num+1,num+1+cnt); cnt=unique(num+1,num+1+cnt)-num-1; } int main() { //freopen("E.in","w",stdout); init(); //printf("cnt=%d\n",cnt); //for(int i=1;i<=40;i++) printf("%d %lld\n",i,(ll)num[i]); for(scanf("%d",&T);T;T--){ ll n; scanf("%lld",&n); ans=upper_bound(num+1,num+1+cnt,n)-num-1; printf("%lld\n",ans); } }
G:Yu Ling(Ling YueZheng) and Colorful Tree
题意:
给定一棵\( n \)个点的树(有边权),给\( q \)次操作,分为两种:
\( 0 , u , x \) :把点\( u \)染成颜色\( x \),这里颜色彼此不同而且范围在\( n \)以内;
\( 1 , u , l , r , x \) :查询离\( u \)最近的、染过色而且颜色\( c \)满足\( l \leq c \leq r , c mod x = 0 \)的祖先到\( u \)的距离。
\( 1 \leq n , q \leq 1.1*10^5 \) 。
分析:
看了第一个通过的队伍的代码,竟然是树状数组套线段树,复杂度\( O(nlog^2(n)) \),很巧妙。
首先,需要处理\( c \% x = 0 \)这个要求——可以离线做,然后把操作分类,将有倍数关系的操作建立关系;暂时可以这样想。
查询要找的是祖先——可以反过来想,染色操作会对被染色点的子树有影响,而操作子树貌似比操作祖先方便一些。
综合起来,我们可以先找到每个颜色的因数,把它们存在vector里;染上某个颜色的操作会影响到该颜色所有的因数,于是把这个操作分别放进它所有因数的存储操作的另一个vector里;当然,查询某个颜色的操作也放进那个颜色的操作vector里;这个vector里的操作要按顺序放。
于是我们可以考虑每个颜色的操作序列;按顺序进行修改和查询操作。
为了方便地对子树整体修改,我们求出这棵树的dfs序,在dfs序列上用树状数组进行区间操作,给染色点的子树进行某些修改。
下面我们考虑\( l \leq c \leq r \)这个要求——如果用线段树维护颜色值序列,那么这个也可以方便地查询;
也就是说,当出现一个查询操作时,我们要在查询点的线段树内查找\( l , r \)范围内是否有数——只要有数即可,因为前面只进行过当前颜色的倍数颜色的染色操作。
现在我们就可以写出树状数组套线段树了:树状数组建在dfs序列上,其中每个点有一个线段树建在颜色数值上;修改操作就是给修改点的子树中每个点的线段树上染色颜色位置\( +1 \),查询操作就是在查询点的线段树上提取\( l , r \)区间的和;
这时查询操作做了一半——我们可以知道对于当前点,有多少个祖先符合条件;
倍增记录的祖先节点,我们可以找一下到哪个祖先节点时,它也有同样个数的祖先符合条件;最浅的那个点就是答案,\( dis[u] \)相减即得距离。
那么空间复杂度怎么办?实际上,我们可以用到线段树节点时再把它开出来;而对于每个颜色的操作我们最多用到\( qlog^2(n) \)个线段树点,所以用时再开,用完删除即可。
时间复杂度是\( O(nlog^2(n)) \)。
(过程中顺便仔细又看了看树状数组,这篇博客挺好的。)
代码如下:
#include<iostream> #include<vector> #define mid ((l+r)>>1) #define pb push_back #define ll long long using namespace std; int const N=110005,M=20000005;/// int n,q,hd[N],nxt[N<<1],cnt,to[N<<1],fa[N][20],dfn[N],ed[N],dcnt; int rt[N],ls[M],rs[M],tcnt,tr[M]; ll w[N<<1],dis[N],ans[N]; vector<int>ele[N]; struct Nd{ int u,l,r,id; }; vector<Nd>v[N]; void init() { for(int x=1;x<=n;x++) for(int j=x;j<=n;j+=x)ele[j].pb(x);//j mod x = 0 同余类 } void add(int x,int y,ll s){nxt[++cnt]=hd[x]; hd[x]=cnt; to[cnt]=y; w[cnt]=s;} void dfs(int u) { dfn[u]=++dcnt; for(int i=hd[u],v;i;i=nxt[i]) { if((v=to[i])==fa[u][0])continue; fa[v][0]=u; dis[v]=dis[u]+w[i]; //dfs(v);// for(int i=1;i<20;i++) fa[v][i]=fa[fa[v][i-1]][i-1]; dfs(v);//注意顺序TAT } ed[u]=dcnt; } //线段树,序列是染色数值 void chg2(int &x,int l,int r,int p,int s) { if(!x)x=++tcnt; tr[x]+=s; if(l==r)return; if(p<=mid)chg2(ls[x],l,mid,p,s); else chg2(rs[x],mid+1,r,p,s); } int qry2(int x,int l,int r,int ql,int qr) { if(!x||(l>=ql&&r<=qr))return tr[x]; int ret=0; if(mid>=ql)ret+=qry2(ls[x],l,mid,ql,qr); if(mid<qr)ret+=qry2(rs[x],mid+1,r,ql,qr); return ret; } //树状数组,序列是dfs序 void chg1(int x,int p,int s){for(;x<=n;x+=(x&(-x)))chg2(rt[x],1,n,p,s);} int qry1(int x,int l,int r){int ret=0; for(;x;x-=(x&-x))ret+=qry2(rt[x],1,n,l,r); return ret;} void dell(int x){for(;x<=n;x+=(x&-x))rt[x]=0;} void del(int x) { while(tcnt)ls[tcnt]=0,rs[tcnt]=0,tr[tcnt]=0,tcnt--; //for(int i=1;i<=n;i++)rt[i]=0; for(Nd it:v[x]) if(it.id==0)dell(dfn[it.u]),dell(ed[it.u]+1); } void work(int x)//树状数组单点修改(差分),区间查询(前缀和) //此处x是颜色 { //printf("x=%d\n",x); for(Nd it:v[x]) { if(it.id==0)//如果是修改,则子树内各点的线段树上所染颜色位置+1 { //printf("stchg\n"); chg1(dfn[it.u],it.l,1); chg1(ed[it.u]+1,it.l,-1); //printf("aftchg\n"); } else//如果是查询 { //printf("stq\n"); int u=it.u,L=it.l,R=it.r,id=it.id; //printf("u=%d L=%d R=%d id=%d\n",u,L,R,id); int num=qry1(dfn[u],L,R),p=u; if(!num){ans[id]=-1; continue;} for(int i=19;i>=0;i--) if(qry1(dfn[fa[p][i]],L,R)==num)p=fa[p][i]; //printf("u=%d p=%d %lld %lld\n",u,p,dis[u],dis[p]); ans[id]=dis[u]-dis[p]; //printf("aftq\n"); } } del(x);//删除线段树 } int main() { scanf("%d%d",&n,&q); init(); for(int i=1,u,v,s;i<n;i++) scanf("%d%d%d",&u,&v,&s),add(u,v,s),add(v,u,s); dfs(1); //v[x]:x的0同余类下的修改与查询操作,而且是按顺序放的 for(int i=1,tp,u,a,b,c;i<=q;i++) { scanf("%d%d%d",&tp,&u,&a); if(tp==0) { for(int it:ele[a])v[it].pb((Nd){u,a,0,0});//it作为询问中的x时可查的点增加了 ans[i]=-2; } else { scanf("%d%d",&b,&c); if(((b+1)/c)==((a-1)/c))ans[i]=-1;//(r+1)/x==(l-1)/x,即不存在l--r中的数mod x = 0 else v[c].pb((Nd){u,a,b,i}); } } for(int x=1;x<=n;x++)work(x);//此处x是颜色 for(int i=1;i<=q;i++) if(ans[i]!=-2) if(ans[i]==-1)printf("Impossible!\n"); else printf("%lld\n",ans[i]); return 0; }
I:Kuriyama Mirai and Exclusive Or
题意:
(见题面)
分析:
(见题解)
做异或的差分,区间修改就给两个端点异或即可;
加长度的异或,可以按\( x \)的每一个二进制为\( 1 \)的位考虑;两段之间异或值就一直是\( +0,+1,+2,+3,......,+2^{i-1} \),可以用\( chg[i][j] \)记录一下;
然后\( chg[i][j] \)处理的时候又可以拆成\( chg[i][j-1], chg[i+2^{j-1}][j-1] \),然后给右半部分区间异或一个\( 2^{j-1} \);因为\( 0, 1, 2, ......, 2^{j-1} \)的左半部分和右半部分的差异仅仅是右半部分的二进制第\( j-1 \)位上都是\( 1 \)而已,异或了以后就可以拆分了。
写得有点不好理解,详细可以再看看题解和代码。
代码如下:
#include<iostream> using namespace std; int const N=6e5+5; int n,q,a[N],sum[N]; bool chg[N][30]; void work(int pos,int x) { for(int i=0,p2;i<30&&pos<=n;i++) if(x&(1<<i)) { chg[pos][i]^=1;//从pos开始异或0,1,2,……,2^i-1(还可以分裂,下面操作) sum[pos]^=((x>>i)<<i);//val,第i位后面全是0 //差分——左端点+ if((p2=pos+(1<<i))<=n)sum[p2]^=((x>>i)<<i);//差分——右端点- pos+=(1<<i); x+=(1<<i);//加了这一段的长度 } } void work2() { for(int i=29;i;i--) for(int j=1,p2,p3;j<=n;j++) if(chg[j][i]) { chg[j][i-1]^=1;//分裂:对于0,1,2,……,2^i-1,若右半的所有数都去掉二进制最高位,则两半是一样的 if((p2=j+(1<<(i-1)))<=n) { chg[p2][i-1]^=1;//分裂之右半部分 sum[p2]^=(1<<(i-1));//差分——左端点+ if((p3=p2+(1<<(i-1)))<=n)sum[p3]^=(1<<(i-1));//差分——右端点- } } } int main() { scanf("%d%d",&n,&q); for(int i=1,x;i<=n;i++)scanf("%d",&a[i]); for(int i=1,tp,l,r,x;i<=q;i++) { scanf("%d%d%d%d",&tp,&l,&r,&x); if(!tp){sum[l]^=x; sum[r+1]^=x; continue;} work(l,x); work(r+1,x+(r-l+1));// l--n, r+1--n } work2(); for(int i=1;i<=n;i++) { sum[i]^=sum[i-1]; a[i]^=sum[i]; printf("%d%c",a[i],(i==n)?'\n':' '); } return 0; }
题意:
给出\(n\)个点的完全图,每条边是黑色或白色;求同色三角形的个数。\(1 \leq n \leq 8000\)。
分析:
从\(1\)到\(n\),记录每个点左边、右边的白边点和黑边点各有多少个,算出不同色三角形的个数;每个三角形被算了两边(三个点中有两个点连了异色边),最后除以二,再从所有三角形中减去即可。
G写了。
#include<bits/stdc++.h> #define ll long long using namespace std; namespace GenHelper { unsigned z1,z2,z3,z4,b,u; unsigned get() { b=((z1<<6)^z1)>>13; z1=((z1&4294967294U)<<18)^b; b=((z2<<2)^z2)>>27; z2=((z2&4294967288U)<<2)^b; b=((z3<<13)^z3)>>21; z3=((z3&4294967280U)<<7)^b; b=((z4<<3)^z4)>>12; z4=((z4&4294967168U)<<13)^b; return (z1^z2^z3^z4); } bool read() { while (!u) u = get(); bool res = u & 1; u >>= 1; return res; } void srand(int x) { z1=x; z2=(~x)^0x233333333U; z3=x^0x1234598766U; z4=(~x)+51; u = 0; } } using namespace GenHelper; int n; ll Tot,Ill; bool Edge[8005][8005]; int Input() { int n, seed; cin >> n >> seed; srand(seed); for (int i = 0; i < n; i++) for (int j = i + 1; j < n; j++) Edge[j][i] = Edge[i][j] = read(); return n; } int main() { n=Input(),Tot=1ll*n*(n-1)*(n-2)/6; for(int i=0;i<n;i++) { ll Rb=0,Rw=0,Lb=0,Lw=0; for(int j=i-1;j>=0;j--) Edge[i][j]?Lb++:Lw++; for(int j=i+1;j<n;j++) Edge[i][j]?Rb++:Rw++; Ill+=Lb*Rw+Lw*Rb+Lw*Lb+Rw*Rb; } printf("%lld\n",Tot-Ill/2); }