2020牛客多校第7场赛后感
本博客主要目的为个人使用,也欢迎读者讨论。
1.题难懂
2.自己傻
3.知识少
A. Social Distancing
题意:在坐标系中原点为圆心,r为半径的圆内放n个点,求$\sum_{i=1}^{n-1} \sum_{j=i+1}^n d(i,j)^2$最大值。选择的坐标是整数坐标。$1 \le n \le 8, 1\le r \le 30$
可以用dp。(这个真没想到,我还以为是贪心)
f[i,x,y]表示用了i个人,x表示横坐标之和,y表示纵坐标之和。
那么$f[i,x,y]=\sum_{i=1}^{n-1} \sum_{j=i+1}^n ((x_i-x_j)^2+(y_i-y_j)^2)=\sum_{i=1}^{n-1} \sum_{j=i+1}^n (x_i^2+x_j^2-2x_ix_j+y_i^2+y_j^2-2y_iy_j)$
可以推出$f[i,x+j,y+k]=max\{f[i-1,x,y]+(i-1)(j^2+k^2)+\frac{f[i-1,x,y]+x^2+y^2}{n-1}-2xj-2yk\}$
如果怕超时的话,可以预处理。
下面是dp的代码:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> #include<cmath> #include<queue> using namespace std; typedef pair<int,int> PII; struct Trip{ int x, y, d; Trip(int x=0, int y=0, int d=0):x(x),y(y),d(d){} }; int n, r; //vector<Trip> f[10]; //vector<int> Pos; int f[10][600][600], base=300; int cal(int x, int r){ if(x<0) x=-x; return sqrt(r*r-x*x); } int main(){ //freopen("A.txt","w",stdout); for(int i=1; i<=30; ++i){ memset(f,0,sizeof(f)); r=i; for(int n=2; n<=8; ++n){ for(int xi=-300; xi<=300; ++xi){ for(int yi=-300; yi<=300; ++yi){ if(n>2 && f[n-1][xi+base][yi+base]==0) continue; if(n==2 && xi*xi+yi*yi>r*r) continue; for(int xii=-r; xii<=r; ++xii){ int yii_max=sqrt(r*r-xii*xii); for(int yii=-yii_max; yii<=yii_max; ++yii){ f[n][xi+xii+base][yi+yii+base]=max(f[n][xi+xii+base][yi+yii+base],\ f[n-1][xi+base][yi+base]+(n-1)*(xii*xii+yii*yii)+(f[n-1][xi+base][yi+base]+xi*xi+yi*yi)/(n-1)-2*xii*xi-2*yii*yi); } } } } int ans=0; for(int i=-300; i<=300; ++i){ for(int j=-300; j<=300; ++j){ ans=max(ans,f[n][i+base][j+base]); } } printf("\t%d", ans); if(n==8) puts(""); } } getchar(); return 0; }
下面是打表的代码:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; int a[10][40]; int main(){ a[1][1]=0; a[2][1]=4; a[3][1]=8; a[4][1]=16; a[5][1]=24; a[6][1]=36; a[7][1]=48; a[8][1]=64; a[1][2]=0; a[2][2]=16; a[3][2]=32; a[4][2]=64; a[5][2]=96; a[6][2]=144; a[7][2]=192; a[8][2]=256; a[1][3]=0; a[2][3]=36; a[3][3]=76; a[4][3]=144; a[5][3]=218; a[6][3]=324; a[7][3]=432; a[8][3]=576; a[1][4]=0; a[2][4]=64; a[3][4]=130; a[4][4]=256; a[5][4]=384; a[6][4]=576; a[7][4]=768; a[8][4]=1024; a[1][5]=0; a[2][5]=100; a[3][5]=224; a[4][5]=400; a[5][5]=624; a[6][5]=900; a[7][5]=1224; a[8][5]=1600; a[1][6]=0; a[2][6]=144; a[3][6]=312; a[4][6]=576; a[5][6]=880; a[6][6]=1296; a[7][6]=1740; a[8][6]=2304; a[1][7]=0; a[2][7]=196; a[3][7]=416; a[4][7]=784; a[5][7]=1188; a[6][7]=1764; a[7][7]=2356; a[8][7]=3136; a[1][8]=0; a[2][8]=256; a[3][8]=554; a[4][8]=1024; a[5][8]=1572; a[6][8]=2304; a[7][8]=3102; a[8][8]=4096; a[1][9]=0; a[2][9]=324; a[3][9]=722; a[4][9]=1296; a[5][9]=2014; a[6][9]=2916; a[7][9]=3954; a[8][9]=5184; a[1][10]=0; a[2][10]=400; a[3][10]=896; a[4][10]=1600; a[5][10]=2496; a[6][10]=3600; a[7][10]=4896; a[8][10]=6400; a[1][11]=0; a[2][11]=484; a[3][11]=1064; a[4][11]=1936; a[5][11]=2984; a[6][11]=4356; a[7][11]=5872; a[8][11]=7744; a[1][12]=0; a[2][12]=576; a[3][12]=1248; a[4][12]=2304; a[5][12]=3520; a[6][12]=5184; a[7][12]=6960; a[8][12]=9216; a[1][13]=0; a[2][13]=676; a[3][13]=1512; a[4][13]=2704; a[5][13]=4224; a[6][13]=6084; a[7][13]=8280; a[8][13]=10816; a[1][14]=0; a[2][14]=784; a[3][14]=1746; a[4][14]=3136; a[5][14]=4870; a[6][14]=7056; a[7][14]=9564; a[8][14]=12544; a[1][15]=0; a[2][15]=900; a[3][15]=2016; a[4][15]=3600; a[5][15]=5616; a[6][15]=8100; a[7][15]=11016; a[8][15]=14400; a[1][16]=0; a[2][16]=1024; a[3][16]=2264; a[4][16]=4096; a[5][16]=6336; a[6][16]=9216; a[7][16]=12456; a[8][16]=16384; a[1][17]=0; a[2][17]=1156; a[3][17]=2600; a[4][17]=4624; a[5][17]=7224; a[6][17]=10404; a[7][17]=14160; a[8][17]=18496; a[1][18]=0; a[2][18]=1296; a[3][18]=2888; a[4][18]=5184; a[5][18]=8056; a[6][18]=11664; a[7][18]=15816; a[8][18]=20736; a[1][19]=0; a[2][19]=1444; a[3][19]=3218; a[4][19]=5776; a[5][19]=9008; a[6][19]=12996; a[7][19]=17666; a[8][19]=23104; a[1][20]=0; a[2][20]=1600; a[3][20]=3584; a[4][20]=6400; a[5][20]=9984; a[6][20]=14400; a[7][20]=19584; a[8][20]=25600; a[1][21]=0; a[2][21]=1764; a[3][21]=3912; a[4][21]=7056; a[5][21]=10942; a[6][21]=15876; a[7][21]=21500; a[8][21]=28224; a[1][22]=0; a[2][22]=1936; a[3][22]=4344; a[4][22]=7744; a[5][22]=12080; a[6][22]=17424; a[7][22]=23688; a[8][22]=30976; a[1][23]=0; a[2][23]=2116; a[3][23]=4712; a[4][23]=8464; a[5][23]=13144; a[6][23]=19044; a[7][23]=25808; a[8][23]=33856; a[1][24]=0; a[2][24]=2304; a[3][24]=5138; a[4][24]=9216; a[5][24]=14326; a[6][24]=20736; a[7][24]=28122; a[8][24]=36864; a[1][25]=0; a[2][25]=2500; a[3][25]=5612; a[4][25]=10000; a[5][25]=15624; a[6][25]=22500; a[7][25]=30624; a[8][25]=40000; a[1][26]=0; a[2][26]=2704; a[3][26]=6062; a[4][26]=10816; a[5][26]=16896; a[6][26]=24336; a[7][26]=33120; a[8][26]=43264; a[1][27]=0; a[2][27]=2916; a[3][27]=6536; a[4][27]=11664; a[5][27]=18184; a[6][27]=26244; a[7][27]=35664; a[8][27]=46656; a[1][28]=0; a[2][28]=3136; a[3][28]=6984; a[4][28]=12544; a[5][28]=19488; a[6][28]=28224; a[7][28]=38266; a[8][28]=50176; a[1][29]=0; a[2][29]=3364; a[3][29]=7520; a[4][29]=13456; a[5][29]=20968; a[6][29]=30276; a[7][29]=41200; a[8][29]=53824; a[1][30]=0; a[2][30]=3600; a[3][30]=8084; a[4][30]=14400; a[5][30]=22480; a[6][30]=32400; a[7][30]=44076; a[8][30]=57600; int T; scanf("%d", &T); for(int i=1; i<=T; ++i){ int n, r; scanf("%d%d", &n, &r); printf("%d\n", a[n][r]); } return 0; }
我还学了学模拟退火,但错误率很高,正确率大约1/3,只可作为参考。可能是我还不会模拟退火。
下面是我模拟退火的代码:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<cmath> #include<ctime> #include<vector> #include<map> using namespace std; typedef pair<int,int> PII; int n, r; int ans[8]; vector<PII> V, V2; map<int,int> M; double calw(int a[]){ double res=0; for(int i=0; i<n-1; ++i){ for(int j=i+1; j<n; ++j){ int tx=V[a[i]].first-V[a[j]].first, ty=V[a[i]].second-V[a[j]].second; res+=tx*tx+ty*ty; } } return res; } void gettmp(int a[], int b[], double T, int sz){ int cnt=0; for(int i=0; i<n; ++i){ do{ a[i]=b[i]+(rand()*2-RAND_MAX)*T; while(a[i]<0) a[i]+=sz; a[i]%=sz; }while(0); M[a[i]]=1; } } void sa(double T, double down, double &answ, int sz){ int cnt=0; double resw=answ; int res[8]; for(int i=0; i<n; ++i) res[i]=ans[i]; while(T>1e-5){ int tmp[8]; M.clear(); gettmp(tmp,res,T,sz); double tw=calw(tmp); if(tw>answ){ for(int i=0; i<n; ++i) ans[i]=res[i]=tmp[i]; answ=resw=tw; }else if(tw>resw || exp(tw-resw/T)*RAND_MAX>rand()){ for(int i=0; i<n; ++i) res[i]=tmp[i]; resw=tw; } T*=down; } } double SA(){ V2.clear(); V.clear(); for(int i=r-1, j=0; i>0; --i){ while(i*i+(j+1)*(j+1)<=r*r) j++; V2.push_back(PII(j,i)); } int sz=V2.size(); V.push_back(PII(0,r)); for(int i=0; i<sz; ++i) V.push_back(V2[i]); V.push_back(PII(r,0)); for(int i=0; i<sz; ++i) V.push_back(PII(V2[i].first,-V2[i].second)); V.push_back(PII(0,-r)); for(int i=0; i<sz; ++i) V.push_back(PII(-V2[i].first,-V2[i].second)); V.push_back(PII(-r,0)); for(int i=0; i<sz; ++i) V.push_back(PII(-V2[i].first,V2[i].second)); sz=V.size(); double T=0.1, down=0.995, answ=calw(ans); for(int i=1; i<=10; ++i){ sa(T,down,answ,sz); } return answ; } int main(){ srand((unsigned)time(NULL)); // freopen("A.txt","w",stdout); for(int i=1; i<=30; ++i){ for(int j=1; j<=8; ++j){ n=j; r=i; for(int i=0; i<n; ++i) ans[i]=0; double answ=SA(); printf("\t%5.0lf", answ); } puts(""); } }
B. Mask Allocation
此题最大困难在于读懂题意。
题意:给两个数a,b,求一个数列,使得数列数之和等于a*b,可拼成a个b或b个a,且字典序最大。$n,m \le 10^4$
假设a<b,那么数列中不能有题超过a的数字。a个a加上a*(b-a)个1一定是可行的。现在保住前面a个a,让后面最大。将b-a看作t,那么现在要求a*t的数列最大,这又回到了原题目,因此可以用循环的形式解决问题。
代码:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> using namespace std; int n, m; vector<int> ans; int main(){ ios::sync_with_stdio(false); int T; cin>>T; while(T--){ ans.clear(); cin>>n>>m; while(n&&m){ if(n>m) swap(n,m); for(int i=1; i<=n; ++i){ ans.push_back(n); } m-=n; } int sz=ans.size(); cout<<sz<<endl; for(int i=0; i<sz; ++i) cout<<ans[i]<<(i==sz-1?'\n':' '); } }
C. A National Pandemic
题意:给一棵n个点的树,初始点权都为0,后有m个操作,含3种类型:"1 x y"表示每个点u点权加上y-dis(x,u),其中dis(x,u)为x到u的距离;"2 x"表示将x点点权变为min(x点权,0);"3 x"表示询问x点权。$1 \le n, m \le 5*10^4, y \le 10^4$
应该算是一道典型的树上的题吧。对于1操作,y-dis(x,u)=y-dep(x)-dep(u)+2*dep(lca);这里只有dep(lca)是x,u共同决定的,做法是将x到根的路径上的所有点权加2,询问时求点到根的点权和即可。对于2,另外设一个数组记录即可。
代码如下:(其中大部分用的是树链剖分模板,只看struct TtoC以外的部分即可)
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> using namespace std; typedef long long LL; const int N=50000+10; int n, m; LL val[N], a[N]; LL sumw, sumdep, cnt; struct Graph{ vector<int> G[N]; void init(){ for(int i=0; i<N; ++i){ G[i].clear(); } } void add(int u, int v){ G[u].push_back(v); G[v].push_back(u); } }; struct TtoC{//树链剖分 TreeToChain Graph g; //树 int r; //根结点 int dfntot; int dfn[N], size[N], son[N], top[N], fa[N], dep[N], nfd[N]; LL sum[N<<2], lazy[N<<2]; void init(int r, Graph &g){ //for(int i=0; i<N; ++i) this->g.G[i].clear(); this->r=r; this->g=g; dfntot=0; memset(dep,0,sizeof(dep)); dep[r]=1; memset(son,0,sizeof(son)); fa[r]=0; memset(lazy,0,sizeof(lazy)); } void start(int r, Graph& g){ init(r,g); dfs1(r); //得到size, fa, dep, son dfs2(r,r); //得到dfn, top build(1,1,n); //线段树的建立 } void dfs1(int u){ size[u]=1; int sz=g.G[u].size(); for(int i=0; i<sz; ++i){ int v=g.G[u][i]; if(dep[v]) continue; dep[v]=dep[u]+1; fa[v]=u; dfs1(v); size[u]+=size[v]; if(son[u]==0 || size[v]>size[son[u]]) son[u]=v; } } void dfs2(int u, int ffa){ dfn[u]=++dfntot; nfd[dfntot]=u; top[u]=ffa; if(son[u]){ dfs2(son[u],ffa); } int sz=g.G[u].size(); for(int i=0; i<sz; ++i){ int v=g.G[u][i]; if(v==fa[u] || v==son[u]) continue; dfs2(v,v); } } void changeuv(int u, int v, int w){ int tu=top[u], tv=top[v]; while(tu!=tv){ if(dep[tu]<dep[tv]){ swap(u,v); swap(tu,tv); } change(1,1,n,dfn[tu],dfn[u],w); u=fa[tu]; tu=top[u]; } if(dep[u]<dep[v]) swap(u,v); change(1,1,n,dfn[v],dfn[u],w); } LL queryuv(int u, int v){ LL res=0; int tu=top[u], tv=top[v]; while(tu!=tv){ if(dep[tu]<dep[tv]){ swap(u,v); swap(tu,tv); } res+=query(1,1,n,dfn[tu],dfn[u]); u=fa[tu]; tu=top[u]; } if(dep[u]<dep[v]) swap(u,v); res+=query(1,1,n,dfn[v],dfn[u]); return res; } //以下为线段树 void build(int rt, int l, int r){ if(l==r){ sum[rt]=val[nfd[l]]; }else{ int mid=l+r>>1; build(rt<<1,l,mid); build(rt<<1|1,mid+1,r); sum[rt]=sum[rt<<1]+sum[rt<<1|1]; } } void change(int rt, int l, int r, int L, int R, int w){ if(R<l || r<L) return; if(L<=l && r<=R){ lazy[rt]+=w; return; } sum[rt]+=w*1ll*(R-L+1); int mid=l+r>>1; if(R<=mid) change(rt<<1,l,mid,L,R,w); else if(L>mid) change(rt<<1|1,mid+1,r,L,R,w); else { change(rt<<1,l,mid,L,mid,w); change(rt<<1|1,mid+1,r,mid+1,R,w); } } LL query(int rt, int l, int r, int L, int R){ if(R<l || r<L) return 0; if(L<=l && r<=R) return sum[rt]+lazy[rt]*(r-l+1); int mid=l+r>>1; LL res=0; if(R<=mid) res=query(rt<<1,l,mid,L,R); else if(L>mid) res=query(rt<<1|1,mid+1,r,L,R); else res=query(rt<<1,l,mid,L,mid)+query(rt<<1|1,mid+1,r,mid+1,R); return res+lazy[rt]*(R-L+1); } }; Graph g; TtoC ttc; LL query(int u){ return sumw-sumdep-cnt*ttc.dep[u]+ttc.queryuv(1,u)+a[u]; } int main(){ int T; scanf("%d", &T); while(T--){ g.init(); memset(val,0,sizeof(val)); memset(a,0,sizeof(a)); sumw=sumdep=cnt=0; scanf("%d%d", &n, &m); for(int i=1, u, v; i<n; ++i){ scanf("%d%d", &u, &v); g.add(u,v); } ttc.start(1,g); for(int i=1; i<=m; ++i){ int opt, x; LL y; scanf("%d", &opt); switch(opt){ case 1: scanf("%d%lld", &x, &y); ttc.changeuv(1,x,2); sumw+=y; sumdep+=ttc.dep[x]; cnt++; break; case 2: scanf("%d", &x); y=query(x); if(y>0) a[x]-=y; break; case 3: scanf("%d", &x); printf("%lld\n", query(x)); break; } } } return 0; }
树上的有关知识该复习了,可以看看 qtree
D. ac
没看,应该是水题
H. Dividing
题意:定义满足以下条件为the Legend Tuple:1.(1,k)是;2.如果(a,b)是,则(a+b,b)也是;3.若(a,b)是,则(a*b,b)也是。现求所有满足$1 \le n \le N, 1\le k \le K$的(n,k)个数。$1 \le N, K \le 10^{12}$
对于固定的k,(n,k)是the legend tuple的充要条件是n%k=1或n%k=0。因此当k=1或2时,所有n都满足;当k>2时,n个数是n/k+(n-1)/k+1。
对于$\sum_{i=1}^k \frac{n}{i} $每项下取整是有方法的,时间复杂度O(logn)。
代码见下:
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; typedef long long LL; const LL P=1e9+7; LL n, k; // n/1 + n/2 + ... + n/k向下取整 LL cal(LL n, LL k){ if(!n||!k) return 0; LL res=0, i, j; for(i=1; i<=k; i=j+1){ j=n/(n/i); res=(res+(n/i)%P*(j-i+1)%P)%P; } return (res-n/(i-1)%P*(i-k-1)%P+P)%P; } //向上取整 LL cal22(LL n, LL k){ if(n==k) return (cal(n-1,k-1)+k)%P; return (cal(n-1,k)+k)%P; } LL shang(LL a, LL b){ return (a+b-1)/b; } int main(){ // freopen("data.txt","r",stdin); // freopen("my.txt","w",stdout); ios::sync_with_stdio(false); cin>>n>>k; if(k<3){ cout<<n*k%P<<endl; return 0; } LL ans; if(n>=k) ans=cal(n,k)+cal22(n,k)-n/2%P-shang(n,2)%P+P+P, ans%=P; else ans=cal(n,n)+cal22(n,n)-n/2%P-shang(n,2)%P+k-n+P+P, ans%=P; cout<<ans<<endl; }
题解用了另一种方法,我没看懂,就不写在这里了。
I. Valuable Forests
题意:定义一个森林的值为森林中所有点的度数的平方和。T次询问,每次求所有含n个带标号的点的森林的值之和,结果对P取模。$1 \le T \le 5000, 1 \le n \le 5000, 1 \le P \le 2^{30} 且为质数$
这题的主要攻克点:
1. Cayley n顶点树数定理:
n个点的带标号树个数为$n^{n-2}$。
证明方法:将一棵树和一个数列$\{a_i,a_2,...a_{n-2}\}$对应起来,其中$1 \le a_i \le n$,因此共$n^{n-2}$个。同时,每个数$i$出现$d_i-1$次,$d_i$是度数。
2. 递推的方式。
具体实现方式一篇题解写的很好,我再写也不过是复述而已:https://blog.nowcoder.net/n/026e1c74bc9949e2b57584ac83074404
下面是我的代码:
//code by cyh #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #include<vector> using namespace std; typedef long long LL; const int N=5000+10; LL p; LL fn[N], fv[N], tn[N], tv[N], tmp[N]; LL fa[N], fb[N], CC[N][N]; LL Pow(LL x, LL n, LL P){ if(n==0) return 1; LL tmp=Pow(x,n/2,P); if(n&1) return tmp*tmp%P*x%P; return tmp*tmp%P; } void init(int P){ LL x, y; fa[0]=1; fn[0]=1; tv[1]=0; tn[0]=tn[1]=1; CC[0][0]=1; for(int i=1; i<N; ++i){ CC[i][0]=1; for(int j=1; j<=i; ++j){ CC[i][j]=(CC[i-1][j]+CC[i-1][j-1])%P; } } for(int i=2; i<N; ++i) tn[i]=Pow(i,i-2,P); for(int n=1; n<N; ++n){ for(int i=0; i<n; ++i) fn[n]=(fn[n]+CC[n-1][i]*fn[n-1-i]%P*tn[i+1]%P)%P; } for(int n=2; n<N; ++n){ tmp[0]=1; for(int i=1; i<=n; ++i) tmp[i]=tmp[i-1]*(n-1)%P; for(int i=0; i<n; ++i) tv[n]=(tv[n]+CC[n-2][i]*(i+1)%P*(i+1)%P*tmp[n-2-i]%P)%P; tv[n]=tv[n]*n%P; } for(int n=1; n<N; ++n){ for(int i=0; i<n; ++i) fv[n]=(fv[n]+CC[n-1][i]*(fv[n-i-1]*tn[i+1]%P+fn[n-i-1]*tv[i+1]%P)%P)%P; } } int main(){ // freopen("data.txt","r",stdin); // freopen("my.txt","w",stdout); int T; scanf("%d%lld", &T, &p); init(p); for(int i=1, x; i<=T; ++i){ scanf("%d", &x); printf("%lld\n", fv[x]); } return 0; } I