还不错的一场。。。每次到我这里都是一个断档,往上差好多分,往下差不了多少
不管怎么说,难得排名稍微好一次。。。
$T2$猜的结论伪了,然后$BFS$还写锅了(萌新刚学OI并不觉得丢人)
其实猜的差不多,稍微弄一弄把$BFS$改一下就$50$了
$T3$留的时间不多然后写$30$的部分分挂了$10$分。结果是$vis$数组忘用了。。。弱智错误还是很多,但是凭借着$T1$乱写$A$了所以苟住了。
然而与「我考好」这件事一样反常,这次不是改题大神了。改题巨慢。。。
因为$T3$差不多是$dy$讲的原题然而当时没有听懂所以今天研究了好长时间。
脑子好慢啊。。。学新东西慢这是个大毛病啊。。。
T1:环
大意:要求构造一个$01$序列,长为$n$且恰好有$k$个$1$。满足其中一个$1$与其右侧的$0$交换后与原序列循环同构。$n le 100,T le 10$
如果$gcd(n,k)$不是$1$那么一定无解。证明很简单,用$1$的下标和算一下。
循环右移下标和增加$kx$。$x$是位移位数。而交换操作会使下标增加$1$。则$kx equiv 1 (mod n)$
顺便还是讲一下我的弱智思路。
我们发现这些$1$中间有若干数量的$0$。我们称之为零段。一共有$k$个。
如果最短的零段长度为$x$那么最长的零段长度一定不会超过$x+1$。否则交换一次不可能仍然循环同构。
然后我们把这些零段固有的$k$都删去,这个新的零段长度序列也就是一个$01$序列了。
我们发现每次交换操作是把一个$1$与左边的$0$交换,而且也要求循环同构。与原来的问题很像$reverse$一下就好了。
所以我们要构造一个新的满足条件的序列,长度为$k$,里面$1$的个数是$n \% k$。
可以递归求解了。和辗转相除是一样的复杂度有个$log$。
1 #include<bits/stdc++.h> 2 using namespace std; 3 int s[111][111],t,l,k,n,res[111],dif[111],ans; 4 int gcd(int x,int y){return y?gcd(y,x%y):x;} 5 void solve(int n,int k,int f){ 6 if(k==1){for(int i=2;i<=n;++i)s[f][i]=0;s[f][1]=1;return;} 7 int a=n/k,e=n%k,c=0; 8 solve(k,e,f+1); 9 reverse(s[f+1]+1,s[f+1]+k+1); 10 for(int i=1;i<=k;++i){ 11 s[f][++c]=1; 12 for(int j=1;j<a+s[f+1][i];++j)s[f][++c]=0; 13 } 14 } 15 bool chk(int st){ 16 int x=0; 17 for(int i=st;i<=n;++i)res[++x]=s[0][i]; 18 for(int i=1;i<st;++i)res[++x]=s[0][i]; 19 for(int i=1;i<=n;++i)dif[i]=res[i]-s[0][i]; 20 int c=0; 21 for(int i=1;i<=n;++i)c+=dif[i]?1:0; 22 if(c!=2)return 0; 23 for(int i=1;i<=n;++i)if(dif[i]==-1&&dif[i%n+1]==1)return 1; 24 return 0; 25 } 26 int main(){ 27 cin>>t;while(t-->0){ 28 cin>>n>>k>>l; 29 if(gcd(n,k)!=1) {puts("NO");continue;} 30 puts("YES"); 31 solve(n,k,0); 32 for(int i=1;i<=n;++i)printf("%d",s[0][i]);puts(""); 33 for(int i=1;i<=n;++i)if(chk(i))ans=i; 34 int bg=1; 35 for(int i=2;i<=l;++i){ 36 bg=(bg+ans-2)%n+1; 37 for(int j=bg;j<=n;++j)printf("%d",s[0][j]); 38 for(int j=1;j<bg;++j)printf("%d",s[0][j]); 39 puts(""); 40 } 41 } 42 }
题解做法相较于这个不知道高明到哪里去了。。。
求个逆元,然后把数填在$frac{0,1,...,k-1}{k}$上。循环位移$frac{1}{k}$位就好。交换$0,1$就好。
就没了。代码也简单得多。。神仙想到的
T2:DNA序列
大意:给定$n$个字符串,要求你把它们的非空前缀任意次序拼接后字典序最小。$n,maxlen le 50$
最简单粗暴的是把所有字符串按照字典序排序,显然是错的。
因为后半截我们不要。。。所以单纯按照字典序排序不对劲。。。
我们发现,如果有几个字符串它们的前缀分别含有字符串$x,z$。而其中前缀为$x$中的字符串的结构如果说是$xxxxy$
那么我们会把这个字符串放在所有前缀为$x$中的最后一个,因为你把所有$x$都放出去之后由于$y<z$所以你会把$y$弄得靠前一些。
这样我们发现一个字符串只要分成两部分就好了,一个是重复若干遍的前缀,一个是剩余部分。
为了方便处理我们给每个字符串后面加上一个极大字符。
我们找到每个串的最短前缀满足$xxx...xx<S$。并提取出剩余部分。
然后就可以按照前缀为第一关键字升序,剩余部分为第二关键字降序排序。
这样我们就知道截前缀的顺序了。
然后应该怎么截。
大神们都写的贪心,我觉得他们特别巨:
排序后倒序考虑所有字符串,每次截取一个前缀当作加入当前答案的前缀更新答案。暴力枚举长度即可。
时间复杂度是$O(n^4)$的
然而像我这种比较笨的嘛。。。当然只能想到$BFS$啦
状态是三维的,$X,Y,L$表示目前填了第$X$串的第$Y$个字符在答案串的$L$位置上。
因为字符串最优字典序平时都是逐位考虑的,所以采用$BFS$。逐位更新。
对于当前的状态前方的串一定与当前的答案串完全契合,如果大了就不会更新答案剪枝,如果小了就直接更新答案
(显然只会更新一位因为$BFS$队列里的元素$L$值至多相差$1$)
也停简单粗暴的。大神都不这么写。。。
因为每次只会更新一位所以状态数与时间复杂度同级。时间复杂度也是$O(n^4)$的
这题对于不会熟练使用$string$的人代码真的是又丑又长。。
1 #include<bits/stdc++.h> 2 using namespace std; 3 string s[55];int len[55],n,anslen,qx[33333333],qy[33333333],ql[33333333],al[55][55][2555];char ans[3333]; 4 struct stf{ 5 string x,r,c; 6 friend bool operator<(stf a,stf b){return a.c<b.c||(a.c==b.c&&a.r>b.r);} 7 }ss[55]; 8 int main(){ 9 cin>>n; 10 for(int i=1;i<=n;++i)cin>>ss[i].x,ss[i].x+='Z'; 11 for(int i=1;i<=n;++i){ 12 int len=ss[i].x.length(); 13 for(int j=1;j<len;++j){ 14 int cnt=0; 15 for(int k=0;k<len;++k)if(ss[i].x[k]<ss[i].x[k%j])goto E;else if(ss[i].x[k]>ss[i].x[k%j])break; 16 for(int k=0;k<len;++k)if(ss[i].x[k]==ss[i].x[k%j])cnt++;else break; 17 cnt/=j; 18 for(int k=0;k<j;++k)ss[i].c+=ss[i].x[k]; 19 for(int k=cnt*j;k<len;++k)ss[i].r+=ss[i].x[k]; 20 ss[i].c+='Z'; 21 break;E:; 22 } 23 } 24 sort(ss+1,ss+1+n); 25 for(int i=1;i<=n;++i)s[i]=ss[i].x,len[i]=s[i].length()-1; 26 s[n+1]+='A'-1;len[n+1]=1; 27 for(int i=1;i<2888;++i)ans[i]='Z'-1; 28 qx[1]=1;qy[1]=0;ql[1]=1; 29 for(int h=1,t=1;h<=t;++h){ 30 int X=qx[h],Y=qy[h],L=ql[h]; 31 if(al[X][Y][L])continue;else al[X][Y][L]=1; 32 if(s[X][Y]<ans[L]){while(ql[t]==L+1)t--;ans[L]=s[X][Y];} 33 if(s[X][Y]==ans[L])if(X!=n+1){ 34 if(Y!=len[X]-1&&s[X][Y+1]<=ans[L+1])qx[++t]=X,qy[t]=Y+1,ql[t]=L+1,ans[L+1]=min(ans[L+1],s[X][Y+1]); 35 if(s[X+1][0]<=ans[L+1])qx[++t]=X+1,qy[t]=0,ql[t]=L+1,ans[L+1]=min(ans[L+1],s[X+1][0]); 36 } 37 }for(int i=1;ans[i]!='A'-1;++i)putchar(ans[i]); 38 }
T3:探寻
大意:有根树,每个边都有代价和收益(先代价后收益)。有一个点为目标点,求从根出发到达目标点的最小初始金钱(可多次随时折返而只付出一次代价)。$n le 200000$
首先稍微操作一下,把目标点的收益改为$inf$。答案就是遍历整棵树的最小初始代价。
如果问题放在序列上,那就是已经做烂了的了。对于所有净收益非负的点按照代价由小到大贪心的吃就好了。
树上的依赖关系,我们考虑用一种类似于延迟标记的方法来解决。(不知道这么说是否合适)
我们依旧把所有点按照上述方法排序,但是这次是动态的所以转而用堆维护($set$平衡树也可以)
考虑一个点现在作为堆顶代价最小会出现什么情况:
1,它的父亲到根的路径上的所有点都已经被经历过了,那么这个点你就可以直接买下来了。和序列的做法一样。
2,如果它到根的路径上的点并没有被经历(经历是指在堆中被取出考虑过),如果往上跳到第一个没有被经历过的点为$fa$。
那么就说明经历$fa$的代价比经历当前点要大且当前点还有净收益。只要$fa$一旦被经历你就会立刻经历这个点,因为可以赚钱。
也就是现在这个点和父亲可以认为是一个套餐,绑定起来了。所以就尝试把这个点合并到$fa$中去考虑。
(1)如果$cost_p > score_{fa}$那么就是说你到达这个点单用父亲的收益还不够还要从外界拿。
那么更新$cost_{fa}=cost_{fa}+cost_p -score_{fa}$,而父亲收益被花掉了而新节点有新收益$score_fa=score_p$
(2)否则,到达父亲就可以直接用在父亲处赚的钱来经历儿子。$score_{fa}=score_{fa}+score_p -cost_p$
这样就可以把当前节点抹掉,更新父节点然后继续考虑了。
找$fa$嘛。。。直接并查集维护就完事了
然后做法就和序列上的差不多了。代码极度好写。。。时间复杂度$O(n log n)$
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define S 233333 4 #define ll long long 5 int n,f[S],al[S],g[S];long long ans,lft,e[S],c[S]; 6 struct P{ 7 ll e,c;int p; 8 friend bool operator<(P x,P y){return x.e>x.c^y.e>y.c?x.e>x.c:(x.c^y.c?x.c<y.c:x.p<y.p);} 9 };set<P>s; 10 int find(int p){return g[p]==p?p:g[p]=find(g[p]);} 11 int main(){ 12 cin>>n;al[0]=1; 13 for(int i=1;i<n;++i)scanf("%d%lld%lld",&f[i],&e[i],&c[i]),e[i]=e[i]==-1?1e16:e[i],s.insert((P){e[i],c[i],i}),g[i]=i; 14 while(!s.empty()){ 15 P x=*s.begin();s.erase(s.begin()); 16 al[x.p]=1;g[x.p]=f[x.p]; 17 if(!al[find(x.p)]){int F=find(x.p); 18 s.erase(s.find((P){e[F],c[F],F}));if(e[F]<x.c)c[F]+=x.c-e[F]; 19 s.insert((P){e[F]>=x.c?(e[F]+=x.e-x.c):e[F]=x.e,c[F],F}); 20 }else{if(lft<x.c)ans+=x.c-lft,lft=x.c;lft+=x.e-x.c;} 21 }cout<<ans<<endl; 22 }