4.21考试题解
1131 Card Manager 题解 这题只有暴力乱搞和正解两种方法 ??分解法 乱搞 20分解法 输出-1 80分解法 考虑将卡牌看成点数字看成边 发现没有什么特殊性质 观察图片 发现除了开头和结尾的两个数外 其他数均出现了偶数次 这像极了欧拉回路中的点度 继续往欧拉回路上想 发现可以以数字为点,以卡片为边建图 问题就变成了输出一张图的欧拉回路/欧拉路径 (欧拉回路见https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%9B%9E%E8%B7%AF/10036484?fr=aladdin) 什么时候有解呢 若一张图联通且任意点度数为偶则存在欧拉回路 若只有两个点度数为奇则可以以这两点为首尾形成欧拉路径 所以无解仅当图不联通或超过两点度数为奇 有解怎么输出呢 有一个算法叫fleury可以求 但是感觉很那个算法写的很冗杂所以不推荐 考虑下面的做法 1.若所有点度数为偶 给图中每条边加一个是否走过的标记 随意选一个点开始搜 只走没走过的边 可以证明若无路可走了一定是回到了原点 回到原点后依次输出每一步访问的点(一个点可能被访问多次) 2.若有两点度数为奇 和上面一样从其中一点开始搜 无路可走时一定是在另一点 同样,输出路径 上面的做法有一个小问题 它不一定遍历完了所有的边 因为无路可走之时往后退几步就有可能有路 怎么改进? 上面我们用的是dfs进栈序并且无路可走就不搜了相当于只找到了一个环/路径 正解使用的是dfs退栈序并且无路可走之时退栈继续搜相当于把多个环接起来(详见代码) 相当于是从终点开始搜到起点 正解是怎么把多个环拼接在一起的? 首先从起点开始搜到终点然后回退,每退一步输出退之前所在的点 退到某个点(设其为A点)发现又有路可走则沿该路又向前搜 则什么时候又会停下来呢,当再一次无路可走时就停下来了 可以证明再次无路可走时一定在A点 此时回退并输出点就相当于把A所在的一个小环与原来的大环相接 当然还有可能环套环套环,递归会解决它的 问题解决了 100分解法 为什么上面只有80分? 考虑上面的复杂度可能会达到O(m^2) 于是加个当前弧优化就可以做到O(m) 相当于把走过的边删掉 最后 考虑到m为1e6 则可能会递归m层 可能会爆栈(然而实际上并没有爆) 如果爆栈就像代码里一样手写一个dfs就好了 代码 #include<bits/stdc++.h> using namespace std; const int N=1000005; vector<int>vec[N]; struct edge{int v,vis,nxt;}e[N*2]; int n,m,st,sz,cnt,d[N],hd[N],z[N*2]; struct Q{int x,y;}q[N]; vector<int>ans; void adde(int u,int v){e[++cnt]=(edge){v,0,hd[u]},hd[u]=cnt;} int tp,stk[N]; void dfs(int u){ stk[++tp]=u; while(tp){ u=stk[tp]; int &i=hd[u]; while(i&&e[i].vis)i=e[i].nxt; if(i){ e[i].vis=e[i^1].vis=1; stk[++tp]=e[i].v; continue; } ans.push_back(z[u]); tp--; } } int main() { cnt=1; cin>>m; for(int i=1;i<=m;i++){ scanf("%d%d",&q[i].x,&q[i].y); z[++sz]=q[i].x,z[++sz]=q[i].y; } sort(z+1,z+1+sz); sz=unique(z+1,z+1+sz)-z-1; random_shuffle(q+1,q+1+m); for(int i=1;i<=m;i++){ Q &qy=q[i]; qy.x=lower_bound(z+1,z+1+sz,qy.x)-z; qy.y=lower_bound(z+1,z+1+sz,qy.y)-z; adde(qy.x,qy.y),adde(qy.y,qy.x); d[qy.x]++,d[qy.y]++; } st=1; int tot=0; for(int i=1;i<=sz;i++)if(d[i]&1)st=i,tot++; if(tot>2)return puts("-1"),0; dfs(st); if(ans.size()!=m+1)return puts("-1"),0; for(int i=0;i<ans.size();i++)printf("%d ",ans[i]); return 0; } 1130 Segment Manager 题解 20分解法 按题意O(n^3)dp即可 60分解法 斜率优化入门题O(n^2) 100分解法 先说40分解法 假设原序列确定 将整个序列分成了m段 那么随着m增大答案单调不升 并且答案的变化率也单调不升 即答案关于m的函数的斜率恒为负且斜率单调不升 这形成了一个凹函数(即(f(x)+f(y))/2>=f((x+y)/2)) 考虑这样一种做法 我们在20分做法里 用dp[i][j]表示到i位选了j段的最小价值 那么我们把第二位去掉,即去掉段数限制 那么这个dp由O(n^3)变为了O(n^2) 此时这个dp求出来的是什么 显然由于答案单减 求出来的是每个数各自一段的总价值,即为0 考虑这样一种处(qi)理(ji)方(yin)法(qiao) 给选择的每一段强行加上一个额外费用w 那么选k段就会有额外k*w的费用 此时答案就不一定是m==n时最优了 换句话说 答案关于m的函数的右端被台升了 及函数的最小值处的横坐标左移了 假设左移后恰好在题中所求的m处最优 那么就可以直接用这种dp求出答案再减去m*w就行了 那我怎么知道w应该取多少m才是最优的? 我不知道,所以二分w 每二分一次就dp算一次最优解并记录此时取了多少段(设有k段) 若k<m,w应该调小 反之应该调大 复杂度O(n^2*logn) 那么100分解法就是把40分解法中的暴力dp换成斜率优化就行了 复杂度O(nlogn) 这种二分好像叫wqs二分(wqs是谁) 很实用但用的人却不多 另外推荐cf739E 可以用wqs套wqs做到比正解优的O(n*(logn)^2) 细节 wqs二分一定能使最后恰好在m处最优吗 不一定,可能并列最优 那如果二分到最后也不是m最优,即l,r中有一个的最优取值与m并列 我怎么知道是l,r中的哪一个? 可以使用代码中的处理方法: 假设m与r的最优决策点的dp值相等 而不是l的最优决策点 那么用l算出来的m的实际dp值一定优过头了 即我们应该取l和r中在m点算出来不太优的那个 #include<bits/stdc++.h> using namespace std; typedef unsigned long long ll; const int N=5.1e5,len=N*3; char str[len],*p=str; int read(){ int x=0; while(*p<'0')++p; while(*p>='0')x=10*x+*p++-'0'; return x; } int n,m,q[N],cnt[N]; ll mid,s[N],_s2[N],_2s[N],dp[N],x[N],y[N]; bool cal(int k,int j,int i){ return y[j]-y[k]<s[i]*(x[j]-x[k]); } bool cal2(int k,int j,int i){ return (y[j]-y[k])*(x[i]-x[j])>(y[i]-y[j])*(x[j]-x[k]); } int Dp(){ int h=1,t=0; q[++t]=0; for(int i=1;i<=n;i++){ while(h<t&&cal(q[h],q[h+1],i))++h; int j=q[h]; dp[i]=dp[j]+mid+(((s[i]-s[j])*(s[i]-s[j])-(_2s[i]-_2s[j]))>>1); cnt[i]=cnt[j]+1; y[i]=(dp[i]<<1)+_s2[i]+_2s[i]; x[i]=s[i]<<1; while(h<t&&cal2(q[t-1],q[t],i))--t; q[++t]=i; } return cnt[n]; } int main()//_2s为平方的和,_s2为和的平方 { fread(str,1,len,stdin); n=read(),m=read(); for(int i=1;i<=n;i++)s[i]=read(),_2s[i]=_2s[i-1]+s[i]*s[i],s[i]+=s[i-1],_s2[i]=s[i]*s[i]; ll l=0,r=_s2[n]; while(l<=r){ mid=(l+r)>>1; int ret=Dp(); if(ret<m)r=mid-1; else if(ret>m)l=mid+1; else l=mid,r=mid-1; } mid=l,Dp(); ll ans1=dp[n]-mid*m; mid=r,Dp(); ll ans2=dp[n]-mid*m; cout<<max(ans1,ans2)<<endl; return 0; } 1129 Matrix Manager 题解 建议不会正解也要写一写30分解法 30分解法 怎么办一看就是毒瘤数据结构 我会分块! 将矩阵分为一个个的正方形 设正方形边长为B 则操作大块的复杂度为O((n/B)^2) 操作块内的复杂度为O(n*B) 即共O((n/B)^2+n*B) 当B==n^(1/3)时取得最优 总复杂度O(q*n^(4/3)) 其实30分可以做到q==1e5 因为打了一下发现0.5s就过了而给了10s 无撤销操作的60分 即需要支持平面加法平面求和 可以用: 1.线段树套线段树 2.区间bit套线段树 3.区间bit套区间bit(二维bit)+hash表 推荐第二种 第一种常数较大并且可能内存吃不消 而且如果非要写线段树套线段树的话 是不能用lazy标记的(至少外层线段树不能用) 因为lazy无法下放 所以非要写第一种就用永久化标记吧,常数小并且还好写一些 100分解法 直接撤销一来不好做二来复杂度承受不起因为会不停的跳来跳去 那么可以想到一道题: https://www.luogu.org/problemnew/show/P1383 难道要写二维主席树之类的恶心玩意? 不用 首先我们给每一个非询问操作一个标号 每个标号就对应了一种版本(类似主席树) 设当前标号为id 每一个修改操作都是以上一个版本(id-1)为基础新建一个版本 那么一个撤销操作相当于是以(id-a-1)号版本为基础新建一个版本 所以还是要主席树? 不用 我们称以i号版本为基础构建的版本为i的儿子 则所有版本构成了一棵树 将这棵树建出来 按dfs的方式遍历 每进入一个修改节点就修改 退出一个修改节点就反向修改 那我们就可以在dfs的过程中回答每个节点包含的询问 就避免了主席树 代码 给出的是第三种做法 #include<cstdio> using namespace std; const int len=1<<22,N=1.1e5,Mod=19999999; typedef long long ll; struct IO{ char sr[len],sw[len],stmp[25],*pr,*pw,*ptmp; IO(){fread(sr,1,len,stdin),pr=sr,pw=sw,ptmp=stmp;} ~IO(){fwrite(sw,1,pw-sw,stdout);} operator int(){ int x=0; while(*pr<'0')++pr; while(*pr>='0')x=(x<<3)+(x<<1)+*pr++-'0'; return x; } void operator = (ll x){ if(!x)*pw++='0'; ll y; while(x)y=x/10,*++ptmp=x-(y<<3)-(y<<1)+'0',x=y; while(ptmp!=stmp)*pw++=*ptmp--; *pw++='\n'; } }io; int n,m,q; struct node{ ll axy,ax,ay,a; void operator += (const node &b){axy+=b.axy,ax+=b.ax,ay+=b.ay,a+=b.a;} }; struct edge{ll v;node w;edge *nxt;}; struct HASH{ edge *hd[Mod],*cnt,e[15000000]; HASH(){cnt=e;} node &adde(int u,ll v){*++cnt=(edge){v,(node){0,0,0,0},hd[u]},hd[u]=cnt;return cnt->w;} node &operator () (int x,int y,bool add){ ll v=ll(x-1)*m+y;int u=(1000000007u*x-998244353*y)%Mod; for(edge *i=hd[u];i;i=i->nxt)if(i->v==v)return i->w; return add?adde(u,v):e->w;//e->w==0 which is only used to read } }; struct BIT{ HASH Hash; void add3(int x,int y,node a){ for(int i=x;i<=n;i+=i&-i) for(int j=y;j<=m;j+=j&-j) Hash(i,j,1)+=a; } void add2(int x,int y,ll a){ add3(x,y,(node){a,a*(1-y),a*(1-x),a*(1-x)*(1-y)}); } void add(int lx,int ly,int rx,int ry,int a){ add2(lx,ly,a); add2(lx,ry+1,-a); add2(rx+1,ly,-a); add2(rx+1,ry+1,a); } ll query2(int x,int y){ node a=(node){0,0,0,0}; for(int i=x;i;i^=i&-i) for(int j=y;j;j^=j&-j) a+=Hash(i,j,0); return a.axy*x*y+a.ax*x+a.ay*y+a.a; } ll query(int lx,int ly,int rx,int ry){ return +query2(rx,ry) -query2(lx-1,ry) -query2(rx,ly-1) +query2(lx-1,ly-1); } }Bit; struct edge2{int v,nxt;}e[N]; struct node2{int typ,lx,ly,rx,ry,fa;ll a;}p[N]; int hd[N],cnt; void adde(int u,int v){e[++cnt]=(edge2){v,hd[u]},hd[u]=cnt;} void dfs(int u){ node2 &a=p[u]; if(a.typ==1)Bit.add(a.lx,a.ly,a.rx,a.ry,a.a); if(a.typ==0)a.a=Bit.query(a.lx,a.ly,a.rx,a.ry); for(int i=hd[u];i;i=e[i].nxt)dfs(e[i].v); if(a.typ==1)Bit.add(a.lx,a.ly,a.rx,a.ry,-a.a); } int main() { n=io,m=io,q=io; for(int i=1,j=0,k=q+1,typ;i<=q;i++){ typ=io; if(typ==0)p[--k]=(node2){typ,io,io,io,io,j,0}; else if(typ==1)p[++j]=(node2){typ,io,io,io,io,j-1,io}; else p[++j]=(node2){typ,0,0,0,0,j-1-io,0}; } for(int i=1;i<=q;i++)adde(p[i].fa,i); p[0].typ=2; dfs(0); for(int i=q;!p[i].typ;i--)io=p[i].a; return 0; }