还没改完题,先留个坑。
放一下AC了的代码,其他东西之后说…
改完了 快下课了先扔代码
跑了跑了 思路慢慢写
来补完了[x
刚刚才发现自己打错了标题
这次考试挺爆炸的XD除了T3老老实实打暴力拿了52分,T1T2都爆了个位数
因为我T1T2,这次没有打暴力…
T1想到了80分的思路,快乐打炸。T2想到了循环节一类的处理,然后也爆炸。之后发现其实有人和我的写法是一样的,但是他们最低拿了40分。
我还不如老老实实打暴力呢我这个只会暴力骗分与正解以及高分解法无缘的选手@#……¥#%&@@…
不知道自己的思维能力提升了没有,因为明显很多人都想到了。但是我的代码实现能力明显极差,这个…该怎么办呢,多刷题?
考完以后发现看不明白题解,好在隔空暴踩我们的大佬依然非常可靠,下午的时候博客就出来了。我跳过T2直接先去看T3,看不明白思路,大约理解代码理解了很久…之后自己敲代码,一些感觉有疑问的地方直接回去看了他的代码。这大约就是我代码能力差的原因?
抄题解看代码是不对的【大声】
于是T2…还是先理解了他的代码才自己写【因为实在不明白一些细节其实本来自己想是不是更珍贵一点】,卡在了只有我才会犯的问题上,发现了自己对于线段树理解的盲点。
怎么说呢——时间不多了,但我的问题依旧紧迫啊——
不是很安心。
T1矩阵游戏:
考试的时候想了个做法,把行列分开考虑,先处理行的影响,再处理列的影响。因为每一行每一列的和可以O(1)计算。然后对于行列的交点,再计算不足或多出的贡献。复杂度和操作次数k有关,最坏应该是500*500。
然后炸了…
正解是推出一个式子。根据题意,我们可以发现答案公式是:
∑ ri ∑ sj * [ (i-1) * m + j ]
(i-1)*m+j是原矩阵的每一项。
把后面拆开可以得到
∑ ri ∑ sj*(i-1)*m + sj*j
发现对于每一行i,我们要统计的j有同样的东西。即对于每一行,sj*j的和都是一样的。
我到这里还是有点懵,然后我继续拆
∑ ri * ∑ sj*j +∑ ri *∑ sj*(i-1)*m
于是后面又可以把(i-1)*m提出来
∑ ri ∑ sj*j +∑ ri*(i-1)*m ∑ sj
于是发现对于每一行,sj的和也是同样的。
那么我们可以O(n)计算出每一列的sj*j以及统计sum1=∑sj*j,sum2=∑sj
然后O(n)计算每一行的贡献,ans=∑ri*sum1+ri*(i-1)*m*sum2
于是时间复杂度为O(n+n)=O(n),完美通过
#include<iostream> #include<cstdio> using namespace std; int n,m,k,x,y; const int mod=1000000007; long long h[1000010],l[1000010],tag1[1000010],tag2[1000010],sum1,sum2,sum; char c[5]; int main() { scanf("%d%d%d",&n,&m,&k); while(k--){ scanf("%s%d%d",c,&x,&y); if(c[0]=='R'){ if(!tag1[x]){ tag1[x]=1; h[x]=1; } h[x]=h[x]*y%mod; } else{ if(!tag2[x]){ tag2[x]=1; l[x]=1; } l[x]=l[x]*y%mod; } } for(int i=1;i<=m;i++)sum1=(sum1+i*(tag2[i]==0?1:l[i])%mod)%mod,sum2=(sum2+(tag2[i]==0?1:l[i]))%mod;//sj*j for(int i=1;i<=n;i++){ sum=(sum+(tag1[i]==0?1:h[i])*sum1%mod+(tag1[i]==0?1:h[i])*m%mod*(i-1)%mod*sum2%mod)%mod; } printf("%lld",sum%mod); return 0; }
实际上考场上十几个人A了这题,想出正解的没有三十也有二十个人了。于是就让对于推理公式以及系统思维一窍不通的我显得非常智障。
还是没有清晰系统的思路,只会一通瞎搞。知识点也乱糟糟地混杂在一起。做题的时候思维发散而没有条理性,导致想到的东西都纠缠在一起,可能各种方法细节互相影响干扰又理不清,再往一个确定的方向推进就很难。
思维问题也很严重啊——
T2跳房子:
考试的时候看了一眼题面,第一反应是肯定有一个循环节。
然后就考虑能不能搞出这个循环节来处理较大的k。怎么办呢,对于每一个位置暴力求它进入循环的位置,走了几步,以及它能走到的循环的周期?
感觉是不是也可以类似记忆化搜索,存一部分内容来优化……尝试了一下,发现暴力求循环节都不会写,搞个ball的优化。
于是非常没有底气地开始敲代码,给出的样例都过了。手造小样例也过了。
由于我基本功极差不会对拍,我永远和大样例无缘…
考完以后果然发现自己的T2炸成了个位数XD
官方正解我没有仔细研究,和我们的做法好像有共通。我跟着大佬们写了类似于置换的东西。
考虑每一列每个位置到下一列的对应位置,这个过程很像置换,不过置换是一一对应的,而这里可以多对一转移。但是整个过程本身,仍然满足结合律。知道了这一列要到达的下一列的位置的下一个位置,就等于知道了这一列走两步的位置。
那么我们求出每一列到下一列的对应转移,然后合并起来。对于每一个询问操作,可以用快速幂处理k中走m步的整块,再处理零散的步数。
合并类置换,以及走零散的步数,这些过程很容易让人想到线段树。用线段树的子节点代表每一列到下一列的置换,向根递归的时候合并一小块一小块的置换,走不满m的步数的时候也可以在线段树上查询走尽量大的小块。
查询步数大于m的时候,先让当前位置走到第一列以方便使用快速幂调用最大的包含整个矩阵的置换。如果剩下的步数仍然大于m,可以用线段树节点b[1],即整个矩阵从第一列走一圈再走回第一列的置换,用类似快速幂的方式算出当前位置。剩下还有不足m的零散步数,直接在线段树上查询。然后对于当前所在列,如果大于m要及时减去。
置换里对应的其实是从当前位置会走到下一列的哪一行,而在代码里我是用y来表示的,回答的时候要反着输出x和y…
对于修改,会发现修改一个位置只可能会影响前一列左上,左,左下三个位置的转移。于是对于每一个修改操作,直接修改矩阵里存的值,然后从线段树的根一路走到前一列对应的叶子节点,类似于建树一样修改。这种修改只会影响子节点到根这一路上的父亲节点,于是也像建树时一样pushup合并一下。
#include<iostream> #include<cstdio> using namespace std; int n,m,q,k,sx=1,sy=1; char c[10]; int a[2210][2210]; struct node{ int l,r,nxt[2210]; }b[8210]; void pushup(int p){ node e1,e2; for(int i=1;i<=n;i++){ e1.nxt[i]=b[p*2].nxt[i]; e2.nxt[i]=b[p*2+1].nxt[i]; } for(int i=1;i<=n;i++){ b[p].nxt[i]=e2.nxt[e1.nxt[i]]; } } void build(int p,int l,int r){ b[p].l=l,b[p].r=r; if(l==r){ for(int i=1;i<=n;i++){ int y=(l==m?1:l+1); int x1=(i==1?n:i-1); int x2=i; int x3=(i==n?1:i+1); int val=0; if(a[x1][y]>val){ val=a[x1][y]; b[p].nxt[i]=x1; } if(a[x2][y]>val){ val=a[x2][y]; b[p].nxt[i]=x2; } if(a[x3][y]>val){ val=a[x3][y]; b[p].nxt[i]=x3; } } return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); pushup(p); } node query(int p,int l,int r){ if(l<=b[p].l&&b[p].r<=r){ return b[p]; } int mid=(b[p].l+b[p].r)/2; if(r<=mid)return query(p*2,l,r); if(l>mid)return query(p*2+1,l,r); node e1=query(p*2,l,r); node e2=query(p*2+1,l,r); node e; for(int i=1;i<=n;i++){ e.nxt[i]=e2.nxt[e1.nxt[i]]; } return e; } node work(node x,node y){ node e; node e1,e2; for(int i=1;i<=n;i++){ e1.nxt[i]=x.nxt[i]; e2.nxt[i]=y.nxt[i]; } for(int i=1;i<=n;i++){ e.nxt[i]=e2.nxt[e1.nxt[i]]; } return e; } node ks(node a,int k){ node num; for(int i=1;i<=n;i++)num.nxt[i]=i; while(k){ if(k&1)num=work(num,a); a=work(a,a); k>>=1; } return num; } void change(int p,int l,int r){ if(l<=b[p].l&&b[p].r<=r){ for(int i=1;i<=n;i++){ int y=(l==m?1:l+1); int x1=(i==1?n:i-1); int x2=i; int x3=(i==n?1:i+1); int val=0; if(a[x1][y]>val){ val=a[x1][y]; b[p].nxt[i]=x1; } if(a[x2][y]>val){ val=a[x2][y]; b[p].nxt[i]=x2; } if(a[x3][y]>val){ val=a[x3][y]; b[p].nxt[i]=x3; } } return; } int mid=(b[p].l+b[p].r)/2; if(l<=mid)change(p*2,l,r); if(r>mid)change(p*2+1,l,r); pushup(p); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ scanf("%d",&a[i][j]); } } build(1,1,m); scanf("%d",&q); while(q--){ scanf("%s",c); if(c[0]=='m'){ scanf("%d",&k); if(!k){ printf("%d %d",sy,sx); continue; } if(sx+k-1<=m){ sy=query(1,sx,sx+k-1).nxt[sy]; sx=sx+k; if(sx>m)sx-=m; } else{ k=k-(m-sx+1); sy=query(1,sx,m).nxt[sy]; sx=1; if(k/m!=0){ sy=ks(b[1],k/m).nxt[sy]; } if(k%m!=0){ k=k%m; sy=query(1,sx,sx+k-1).nxt[sy]; sx=sx+k; if(sx>m)sx-=m; } } printf("%d %d ",sy,sx); } else{ int x,y,z; scanf("%d%d%d",&x,&y,&z); a[x][y]=z; change(1,(y==1?m:y-1),(y==1?m:y-1)); } } return 0; }
我当时好像根本没有学好群论和置换,然后吃报应的时候来了
怎么说呢,要补的课又是一 大 堆。感谢最近的多次考试,让我不停地发现自己的知识盲区以及自己有多么垃圾orz
T3优美序列:
啊是一道要写一大堆的题…
考试的时候第一次瞅这题题面,一看数据范围,先送给我们50分。
当时起了放弃正解的念头【干什么】
最后发现我这个决定可能是正确的…第三题打炸就真的没分了。虽然我大概也想不到高分解法。
官方正解是分治,和奇袭那道题有点像。抱着锻炼自己代码能力的心思我鼓足一股气试着自己码这个分治,存递归到两边的询问就很智障地用结构体挣扎…也不知道这样对不对。结果越往后面写越不知道对于每一个中间的询问怎么确定它的答案,计算出中间的优美区间怎么更新询问答案,扫一遍然后复杂度爆炸?
emmmmm,一筹莫展。
于是打开了某学车大佬的博客,发现几分钟前他更新了一篇题解,是一道线段树优化建图的题。线段树优化建图?再看题目和本题题面没有半点联系,于是顺手打开洛谷试图求一波大佬的考试题解。
刚求完回去一刷新发现大佬已经更新了…打开往下翻,第三题请跳转另一道题题解。
嗯?这不就是刚刚那道?
线段树优化建图?????
这题解法不唯一。分治是一种,洛谷还有两位大佬用了扫描,然后统计无序二元组等信息或者别的什么的做法。
但我看到的这篇题解的做法是来自于另一个神仙的。主要思路是利用限制关系来点到区间连边建图,然后缩点。
对于原序列里相邻的两个数,如果它们存在于同一个优美序列里,可以得知它们数值之间的所有数都必须要出现。例如对于样例3 1 7 5 6 4 2,如果3 1在同一个优美序列里,那么2也要出现。如果1 7在同一个优美序列里,那么2 3 4 5 6都要出现。
如此一来限制关系就找到了。设每个点对应相邻的两个值,即设点i对应a[i-1]和a[i],那么点i如果出现在优美序列里,相应的一段区间也必须出现。例如,点2,即3 1出现,那么[1,7]这段区间就必须出现。
利用这个限制关系,可以点向区间连边。而这些限制关系可能是成环的,这个位置出现,那么另一个位置也要出现,而另一个位置可能又对应其他位置…一直到最后回到最开始的点,发现这些位置都需要同时出现。那么这些信息就可以利用缩点维护起来,维护对于一个成环的关系,最小会包含哪个区间,即要求哪个区间出现。
我们先对于每一个位置,即原序列里的下标,在线段树上找出对应的子节点。线段树优化建图,线段树里最下面的那一层叶子节点就是原序列,它们之后需要向上连更大的区间。
但是我们建图的限制关系和位置对应的数值有关。对于代表的值为1 3的这个节点来说,它要连向1-3这个连续数值区间对应的最小位置范围,那么我们就需要办法确定1-3这个连续数值区间所需的位置范围。
那么我们需要另一棵树,子节点对应的是数值而不是原序列的位置。把原序列每个数以及所在的位置下标传进树里,存下这个值对应的位置。然后线段树往上合并,就知道连续一段数值区间所需范围的左右端点。
然后设每个点对应相邻的两个数值,这个点的数值上下界就是这两个值,在刚刚的那棵树上找到它数值上下界所对应的位置区间。然后再在最开始下标为位置的那棵树上找到这个位置区间能包含的节点,使当前这个点向所有能包含的节点连边。
这就是建图过程。接下来缩点。
普通地跑一个tarjan缩点,对于每一个scc记录下它能到达的scc,并先用这个scc中的点更新自身能包含的位置范围。然后dfs从一个scc跑到能到达的其它scc,更新每个scc能到达的位置范围。
最后我们还需要一棵线段树,下标仍然是位置,存的是和数值那棵线段树一样的,每个节点对应的位置范围。这一次对于每个位置,存它所在的scc的位置范围。
最后对于每一个询问l,r,在这棵线段树上查询所需范围就可以了。
#include<iostream> #include<cstdio> #include<vector> #include<cstring> using namespace std; int n,a[402010],m,pos[402010],N,dfn[402010],low[402010]; int stack[402010],st,tim,cnt,c[402010],vis[402010],vis1[402010]; vector<int>s1[402010],s2[402010]; struct node{ int l,r; }t1[402010],t2[402010]; node work(node x,node y){ node e; if(x.l>0&&y.l>0)e.l=min(x.l,y.l); else if(x.l>0)e.l=x.l; else e.l=y.l; e.r=max(x.r,y.r); return e; } struct tree{ node t[402010]; void change(int p,int x,node y,int l,int r){ if(l==r){ t[p]=y; return; } int mid=(l+r)/2; if(x<=mid){ change(p*2,x,y,l,mid); } else{ change(p*2+1,x,y,mid+1,r); } t[p]=work(t[p*2],t[p*2+1]); } node query(int p,int l,int r,int L,int R){ if(l<=L&&R<=r){ return t[p]; } int mid=(L+R)/2; if(l<=mid&&r>mid){ node e; e=work(query(p*2+1,l,r,mid+1,R),query(p*2,l,r,L,mid)); return e; } else if(l<=mid)return query(p*2,l,r,L,mid); else if(r>mid)return query(p*2+1,l,r,mid+1,R); } }seg[2]; void build(int p,int l,int r){ if(l==r){ pos[l]=p; return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); s1[p].push_back(p*2); s1[p].push_back(p*2+1); } void addedge(int p,int l,int r,int L,int R,int nod){ if(l<=L&&R<=r){ s1[nod].push_back(p); return; } int mid=(L+R)/2; if(l<=mid)addedge(p*2,l,r,L,mid,nod); if(r>mid)addedge(p*2+1,l,r,mid+1,R,nod); } void tarjan(int x){ dfn[x]=low[x]=++tim; stack[++st]=x; vis1[x]=1; for(int i=0;i<s1[x].size();i++){ int y=s1[x][i]; if(!dfn[y]){ tarjan(y); low[x]=min(low[x],low[y]); } else if(vis1[y])low[x]=min(low[x],dfn[y]); } if(low[x]==dfn[x]){ int p; cnt++; do{ p=stack[st--]; vis1[p]=0; c[p]=cnt; }while(p!=x); } } void dfs(int x){ if(vis[x])return; vis[x]=1; for(int i=0;i<s2[x].size();i++){ int y=s2[x][i]; dfs(y); t2[x]=work(t2[x],t2[y]); } } int main() { scanf("%d",&n); N=4*n+1000; for(int i=1;i<=n;i++)scanf("%d",&a[i]); build(1,1,n); for(int i=1;i<=n;i++){ node y; y.l=i,y.r=i; seg[0].change(1,a[i],y,1,n); } for(int i=2;i<=n;i++){ int x=min(a[i-1],a[i]),y=max(a[i-1],a[i]); t1[pos[i]]=seg[0].query(1,x,y,1,n); addedge(1,t1[pos[i]].l+1,t1[pos[i]].r,1,n,pos[i]); } for(int i=1;i<N;i++){ if(!dfn[i])tarjan(i); } for(int i=1;i<N;i++){ for(int j=0;j<s1[i].size();j++){ int y=s1[i][j]; if(c[i]!=c[y]){ s2[c[i]].push_back(c[y]); } } } for(int i=1;i<N;i++){ t2[c[i]]=work(t2[c[i]],t1[i]); } for(int i=1;i<=cnt;i++)dfs(i); for(int i=2;i<=n;i++){ seg[1].change(1,i,t2[c[pos[i]]],1,n); } scanf("%d",&m); while(m--){ int x,y; scanf("%d%d",&x,&y); if(x==y){ printf("%d %d ",x,y); continue; } node ans=seg[1].query(1,x+1,y,1,n); printf("%d %d ",ans.l,ans.r); } return 0; }
需要注意的有几个地方XD
首先大约是只有我自己才会犯的…有向图scc缩点是需要vis标记的,保证用栈里的点更新low值,不然可能会被横叉边更新QAQ
然后是这里由于每个点代表的是两个相邻的值,所以点向区间连边以及后面询问的时候左端点都要+1
最后别忘记优化建图的那棵线段树要从父亲到儿子连边
大约就是这样了XD
思维难度+,代码难度+++++?需要时常回来看一看这道题qwq
总之最近暴露出的问题非常多——还是继续努力吧,时间真的不多了,各种意义上
写这篇花了半个上午啦,滚去做题了,祝自己和诸位rp++