20230706-NOIP模拟赛

发布时间 2023-07-07 14:31:10作者: H_W_Y

20230706

T1.骰子游戏(dice)

题目大意

给你两个正整数 \(n\)\(d\),你需要构造 \(n\) 组数据,每组6个整数
满足整数都在 \([0, 10^6]\) 范围内,每组数据中两两不同,
在每组数据中分别随机选一个数所得到的异或和为\(d\)的倍数

如果能构造出这样的 \(n\) 组数据,请先输出 ‘Yes’,随后输出你的构造。
如果无法构造出这样的 \(n\) 组数据,请输出 ‘No’。
\(1 \le n \le 100 ,2 \le d \le 60\)

考场思路

由于花在T2的时间过多,所以这道题也就没有什么时间的
发现\(d\)很小,我也尝试着去把它转化成了二进制
同时我也拿\((5)_ {10}=(101)_ 2\) 进行了举例
发现\(101101\)是5的倍数,就想着把\(d\)向左移动几位
到目前为止的想法还是正确的
但是我慌了,于是与正确的做法擦肩而过
心里还惦记着没调出来的T2
等我回来再思考时就换了一种想法
只打了个暴力,还打错了QAQ

Solution

考虑到\(d\)很小,不超过60
我们把\(d\)转化成二进制,就只有6位
而每个数不超过\(10^6\),转化成二进制有20位
我们考虑如何构造\(d\)的倍数
发现把\(d\)进行左移后得到的都是它的倍数
如果我们把它左移6位,与其异或就还是会得到\(d\)的倍数
就顺着这个思路想下去
我们的目的是构造6个数使得它们不管怎么异或都会得到\(d\)的倍数
这样我们每一个骰子都用这6个数即可

说得更简洁一点
我们可以把它们转化到64位进制去构造
使得每一位都是\(0\)\(d\)即可
这样一个骰子的六个面分别是\(0,d,64d,65d,4096d,4097d\)
image
就是要保证对于前面两位都相同是
第三位一个是0一个是\(d\)
这样\(d\)就不会被异或掉

代码就很简单了(伤害性不大,侮辱性极强)

#include <bits/stdc++.h>
using namespace std;

int n,d;

int main(){
  freopen("dice.in","r",stdin);
  freopen("dice.out","w",stdout);
  scanf("%d%d",&n,&d);
  printf("Yes\n");
  for(int i=1;i<=n;i++)
    printf("0 %d %d %d %d %d\n",d,d*64,d*65,d*4096,d*4097);
  return 0;
}

T2.裁剪彩带(cut)

题目大意

长度为\(n\)的序列,把它分割成若干部分
每一部分的美丽值为这个区间的mex
整个序列的美丽度为区间美丽值之和
求最大的美丽度
\(1 \le n \le 5* 10^5,0 \le a_i \le 20\)

考场思路

可能是因为这段时间数据结构写得太多了,尤其是线段树
于是乎,看到mex就想到用主席树去维护
然后再想到一个dp去完成
在发现了\(a_i \le 20\)时依旧很固执地去用主席树维护mex
然后写了一个dp
\(dp[i]=max_{0 \le j \lt i}{dp[j]+query(j+1,i)}\)
然后开始考虑如何优化dp
我先推了个斜率优化(我的脑洞真大……草稿纸写了整整一页)
发现好像是每个数只会被淘汰一次
用单调队列来维护
于是又写了一个自以为很正确的代码
发现过不了大一点的样例,答案错误

于是乎,我开始写暴力
\(n^2\)的dp与前面的代码进行对拍
终于发现哪里不太对了——
\(query(j+1,i)\)不一定单调
所以说我错了
前前后后一共用了两个小时
最后还是用了暴力的代码
结果——MLE(我笑了)
(有一种可能是我写了inline,用空间换时间,再也不写了。。。)

这个经历告诉我们:
不要死磕一道题,其他题说不定更能突破!

Solution

山重水复疑无路,柳暗花明又一村。
有些时候啊
要学会换一种方式来思考问题
好久没打模拟赛,这种思维方式都要忘了

想到dp是没有问题的
我的想法是枚举\(j\)去找每一个\(query(j+1,i)\)
可是发现\(1 \le a_i \le 20\)
mex最大也才21呀
也就是你枚举那么多个\(j\)
可是\(query(j+1,i)\)永远都小于等于21
那就有很多没有用的

这个时候我们就应该想到换一种思路来做
为什么不可以枚举mex?
而又因为\(dp[i]\)一定是单调不下降的
所以我们只要找到满足\(mex= 0 \sim 21\)的每一个最大的\(j\)即可

对于每一个数,我们记录它上一次出现的位置\(lst[a[i]]\)
我们假设现在要去找到\(mex=6\)
那么最大的\(j=min(lst[0],lst[1] \dots lst[5])-1\)
这样再更新答案即可

而对于\(j\)的求解
我们可以再维护一个前缀min即可O(1)得到
所以总复杂度是\(O(22* n)\)

代码也是比较好写的,~

#include <bits/stdc++.h>
using namespace std;

const int maxn=5e5+10;
int n,x,dp[maxn],lst[maxn],sum[maxn],l[maxn],cnt=0,ans[maxn];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

int main(){
  freopen("cut.in","r",stdin);
  freopen("cut.out","w",stdout);
  n=read();
  for(int i=1;i<=n;i++){
  	x=read();
  	lst[x]=i;sum[0]=lst[0];
  	for(int j=1;j<=21;j++) sum[j]=min(sum[j-1],lst[j]);
  	for(int j=1;j<=21;j++)
	  if(sum[j-1]>0)
	  	if(dp[sum[j-1]-1]+j>dp[i]) 
		  dp[i]=max(dp[i],dp[sum[j-1]-1]+j),l[i]=sum[j-1]-1;
  }
  x=n;
  while(x!=0) ans[++cnt]=l[x]+1,x=l[x];
  printf("%d %d\n",dp[n],cnt);
  for(int i=cnt;i>=1;i--) printf("%d ",ans[i]);
  printf("\n");
  return 0;
} 

T3.挑战NPC(hamilton)

题目大意

给你 \(n\) 个点,\(m\) 条边的有向图,求出若干两两不交(没有公共点或公共边)的简单回路的并,使得经过每个点恰好一次。或报告无解。
如果有解,输出任意一个即可。
\(1 \lt n \le 10^5,n \le m \le 2* 10^5\)

考场思路

输出\(NO\)有15分
我是真的在自己很慌的情况下不知道怎么下手……

Solution

网络流(啊这)
我们可以把题意转化成
选出一些边使得每个点的入度和出度都为1
这样就可以进行拆点的网络流
(虽然还没做过数据这么大的网络流,反正网络流的复杂度是\(O(跑得过)\)

我们对于每一个点把它拆成两个点\(i\)\(i+n\)
一个作为出点,一个作为入点
从而防止一个单独的点误认为环
对于每一条\(a \to b\)的有向边
我们考虑在网络流的图上建立\(a \to b+n\)的边
最后在建立一个超级原点和超级汇点
每一条边的最大流量都为1来跑网络最大流即可

而如果最大流等于\(n\)
说明有解,记录一下用过的边来找答案
反之,就无解
(这么就没写网络流都要不会了……但还是写出来了)

#include <bits/stdc++.h>
using namespace std;

const int maxn=2e5+10,inf=0x3f3f3f3f;
int n,m,head[maxn],tot=0,u,v,ans=0,cur[maxn],lv[maxn],t[maxn],st,ed,a[maxn],cnt;
bool vis[maxn];
struct edge{
  int u,v,nxt,val,flag;
}e[maxn];

void add(int u,int v){
  e[++tot]=(edge){u,v,head[u],1,true};
  head[u]=tot; 
  e[++tot]=(edge){v,u,head[v],0,false};
  head[v]=tot;
} 

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

bool bfs(){
  queue<int >q;
  q.push(st);
  memset(lv,-1,sizeof(lv));
  lv[st]=0;cur[st]=head[st];
  while(!q.empty()){
  	int nw=q.front();q.pop();
  	for(int i=head[nw];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,val=e[i].val;
	  if(val>0&&lv[v]==-1){
	  	lv[v]=lv[nw]+1;
	  	q.push(v);
	  }	
	}
  }
  return lv[ed]!=-1;
} 

int dfs(int nw,int flow){
  if(nw==ed) return flow;
  int res=flow;
  for(int i=head[nw];i!=-1;i=e[i].nxt){
  	int v=e[i].v,val=e[i].val;
  	if(val>0&&lv[v]==lv[nw]+1){
  	  int c=dfs(v,min(res,val));
	  res-=c;
	  e[i].val-=c;
	  e[i^1].val+=c;
	}
  }
  return flow-res;
}

void dinic(){
  ans=0;
  while(bfs()) ans+=dfs(st,inf);
}

void dfsans(int x){
  if(vis[x]) return;
  vis[x]=true;
  a[++cnt]=x;
  dfsans(t[x]);
  return;
}

int main(){
  freopen("hamilton.in","r",stdin);
  freopen("hamilton.out","w",stdout);
  memset(head,-1,sizeof(head));tot=-1;
  n=read();m=read();
  st=0;ed=n*2+1;
  for(int i=1;i<=m;i++){
  	u=read();v=read();
  	add(u,v+n);
  }
  for(int i=1;i<=n;i++) add(st,i),add(i+n,ed);
  dinic();
  if(ans<n) printf("NO\n");
  else{
  	printf("YES\n");
  	for(int i=0;i<=tot;i++)
  	  if(e[i].flag==true&&e[i].val==0&&e[i].u!=st&&e[i].v!=ed) t[e[i].u]=e[i].v-n;
  	for(int i=1;i<=n;i++)
  	  if(!vis[i]){
	    cnt=0;
	    dfsans(i);
	    printf("%d ",cnt);
	    for(int i=1;i<=cnt;i++) printf("%d ",a[i]);
	    printf("\n");
	  }
  }
  return 0;
}

T4.无聊的问题

题目大意

有一个长度为 \(n\) 的序列,每个数为 \(a_i\)
现在有 \(q\) 个询问,每个询问先给出一个数 \(k\),然后给出一些区间 \(l_i, r_i\) 求:
\(lcm_{1 \le x \le k}(lcm_{l_x \le i \le r_x} a_i) mod 998244353\)
\(1\le n,q,a_i \le 10^5,1 \le \sum k \le 2.2 * 10^5\)

考场思路

就说我吧
考场上甚至没有反应过来lcm是什么
我以为成了lcp,求两个字符串的最长公共前缀
我挺纳闷的,咋就和字符串扯上关系了……
唉,是完蛋了……
科普一个冷知识:
lcm是最小公倍数
(其实我以前是知道的)
于是我什么都没打

Solution

对于有关lcm的题
我们有一个套路:
分解质因数来考虑

比如,有两个数\(a,b\)
\(a=p_1^{c_1} * p_2^{c_2} * \dots * p_n^{c_n}\)
\(b=p_1^{d_1} * p_2^{d_2} * \dots * p_n^{d_n}\)
那么
\(lcm(a,b)=p_1^{max(c_1,d_1)} * p_2 ^{max(c_2,d_2)} * \dots * p_n^{max(c_n,d_n)}\)

同理,我们可以用这种方法算到\(k\)个数的lcm
而注意到分解质因数\(n\)
有一个很有用的性质——$ \gt \sqrt{n}$的质因数只有一个

先来讨论\(\le \sqrt{n}\)的质数
由于\(a_i \le 10^5\)
所以小于\(sqer{10^5}=316\)的质数只有65个
所以可以直接暴力枚举每一个数出现次数的max
可以直接用线段树或ST表(虽然我不会)维护即可

再来讨论大于316的质数
由于每一个数里这样的质因数只会存在一个
考虑用bitset来维护
我是真的没看懂题解,而且不配代码……
但是前面的思路大概还是懂了,收货还是挺大的

总结

  1. 打模拟赛要合理安排时间,想清楚一道题再动手,而且不能死磕一道题
    否则就会打出这场很可怕的,非常可怕的,极其可怕的模拟赛
  2. 有些时候要学会换个思维方式来思考,考虑去枚举比较小的
  3. 关于lcm的套路——分解质因数(见T4)
  4. 遇到不会的题可以考虑网络流,先把所有暴力打了