暑假集训CSP提高模拟26

暑假集训CSP提高模拟26

\(T1'\) 前缀 \(0pts\)

  • 原题: [ABC366D] Cuboid Sum Query
  • 三维前缀和板子。
    • 由容斥原理,容易有 \(s_{i,j,k}=s_{i,j,k-1}+s_{i,j-1,k}+s_{i-1,j,k}-s_{i-1,j-1,k}-s_{i-1,j,k-1}-s_{i,j-1,k-1}+s_{i-1,j-1,k-1}+a_{i,j,k}\)

    • 类似地,询问时 \(s_{r_{x},r_{y},r_{z}}-s_{r_{x},r_{y},l_{z}-1}-s_{r_{x},l_{y}-1,r_{z}}-s_{l_{x}-1,r_{y},r_{z}}+s_{l_{x}-1,l_{y}-1,r_{z}}+s_{l_{x}-1,r_{y},l_{z}-1}+s_{r_{x},l_{y}-1,l_{z}-1}-s_{l_{x}-1,l_{y}-1,l_{z}-1}\) 即为所求。

      点击查看代码
      int a[110][110][110],s[110][110][110];
      int main()
      {
      	int n,m,lx,ly,lz,rx,ry,rz,i,j,k;
      	cin>>n;
      	for(i=1;i<=n;i++)
      	{
      		for(j=1;j<=n;j++)
      		{
      			for(k=1;k<=n;k++)
      			{
      				cin>>a[i][j][k];
      				s[i][j][k]=s[i][j][k-1]+s[i][j-1][k]+s[i-1][j][k]-s[i-1][j-1][k]-s[i-1][j][k-1]-s[i][j-1][k-1]+s[i-1][j-1][k-1]+a[i][j][k];
      			}
      		}
      	}
      	cin>>m;
      	for(i=1;i<=m;i++)
      	{
      		cin>>lx>>rx>>ly>>ry>>lz>>rz;
      		cout<<s[rx][ry][rz]-s[rx][ry][lz-1]-s[rx][ly-1][rz]-s[lx-1][ry][rz]+s[lx-1][ly-1][rz]+s[lx-1][ry][lz-1]+s[rx][ly-1][lz-1]-s[lx-1][ly-1][lz-1]<<endl;
      	}
      	return 0;
      }
      
  • 枚举一维,另外的二维前缀和维护。

\(T2'\) P270.树 \(100pts\)

\(T3'\) P271.背包 \(100pts\)

\(T4'\) P272.序列 \(0pts\)

  • 第一问在第二问的基础上删除了 \(\gcd(a_{1},a_{2},a_{3}, \dots ,a_{n})=1\) 的限制。

  • 第二问: CF1559E Mocha and Stars

  • 看到 \(\gcd\) 考虑莫反。

  • 推下式子,有 \(\begin{aligned} &\sum\limits_{a_{1}=l_{1}}^{r_{1}}\sum\limits_{a_{2}=l_{2}}^{r_{2}} \dots \sum\limits_{a_{n}=l_{n}}^{r_{n}}[\sum\limits_{i=1}^{n}a_{i} \le m \land \gcd(a_{1},a_{2},a_{3}, \dots ,a_{n})=1] \\ &=\sum\limits_{d=1}^{m}\mu(d)\sum\limits_{a_{1}=l_{1}}^{r_{1}}[d|a_{1}]\sum\limits_{a_{2}=l_{2}}^{r_{2}}[d|a_{2}] \dots \sum\limits_{a_{n}=l_{n}}^{r_{n}}[d|a_{n}] \times [\sum\limits_{i=1}^{n}a_{i} \le m] \\ &=\sum\limits_{d=1}^{m}\mu(d)\sum\limits_{a_{1}=\left\lceil \frac{l_{1}}{d} \right\rceil}^{\left\lfloor \frac{r_{1}}{d} \right\rfloor}\sum\limits_{a_{2}=\left\lceil \frac{l_{2}}{d} \right\rceil}^{\left\lfloor \frac{r_{2}}{d} \right\rfloor}\dots \sum\limits_{a_{n}=\left\lceil \frac{l_{n}}{d} \right\rceil}^{\left\lfloor \frac{r_{n}}{d} \right\rfloor}[\sum\limits_{i=1}^{n}a_{i} \le \left\lfloor \frac{m}{d} \right\rfloor] \end{aligned}\)

  • 提出后半部分,设 \(f_{i,j}\) 表示前 \(i\) 个数中总和为 \(j\) 的方案数,状态转移方程为 \(f_{i,j}=\sum\limits_{k=l_{i}}^{\min(j,r_{i})}f_{i-1,j-k}\) 。前缀和优化即可。

  • 因为有调和级数所以时间复杂度是正确的。

    点击查看代码
    const ll p=998244353;
    ll f[2][100010],s[100010],l[100010],r[100010],nl[100010],nr[100010],prime[100010],vis[100010],miu[100010],len=0;
    void isprime(ll n)
    {
    	memset(vis,0,sizeof(vis));
    	miu[1]=1;
    	for(ll i=2;i<=n;i++)
    	{
    		if(vis[i]==0)
    		{
    			len++;
    			prime[len]=i;
    			miu[i]=-1;
    		}
    		for(ll j=1;j<=len&&i*prime[j]<=n;j++)
    		{
    			vis[i*prime[j]]=1;
    			if(i%prime[j]==0)
    			{
    				miu[i*prime[j]]=0;
    				break;
    			}
    			else
    			{
    				miu[i*prime[j]]=-miu[i];
    			}
    		}
    	}
    }
    ll ask(ll n,ll m)
    {
    	f[0][0]=1;
    	for(ll i=1;i<=m;i++)
    	{
    		f[0][i]=0;
    	}
    	for(ll i=1;i<=n;i++)
    	{
    		for(ll j=0;j<=m;j++)
    		{
    			s[j]=((j-1>=0?s[j-1]:0)+f[(i-1)&1][j])%p;
    			f[i&1][j]=0;
    		}
    		for(ll j=nl[i];j<=m;j++)
    		{
    			f[i&1][j]=(s[j-nl[i]]-((j-nr[i]-1>=0)?s[j-nr[i]-1]:0)+p)%p;
    		}
    	}
    	ll ans=0;
    	for(ll i=0;i<=m;i++)
    	{
    		ans=(ans+f[n&1][i])%p;
    	}
    	return ans;	
    }
    int main()
    {
    	ll n,m,ans=0,i,j;
    	scanf("%lld%lld",&n,&m);
    	isprime(m);
    	for(i=1;i<=n;i++)
    	{
    		scanf("%lld%lld",&l[i],&r[i]);
    	}
    	for(i=1;i<=m;i++)
    	{
    		if(miu[i]!=0)
    		{
    			for(j=1;j<=n;j++)
    			{
    				nl[j]=ceil(1.0*l[j]/i);
    				nr[j]=r[j]/i;
    			}
    			ans=(ans+miu[i]*ask(n,m/i)+p)%p;
    		}
    	}
    	printf("%lld\n",ans);
    	return 0;
    }
    

\(T1\) P273. 博弈 \(0pts\)

  • 原题: 2022牛客OI赛前集训营-提高组(第四场) A 博弈

  • \(cnt_{i}\) 表示路径 \(u \to v\) 的路径上 \(i\) 的出现次数。

  • CF1990A Submission Bait ,不难发现对于点对 \((u,v)\) Crying 必胜当且仅当 \(\exists w \in [0,10^{9}],cnt_{i} \bmod 2=1\)

  • 部分分

    • \(30pts\) :枚举所有点对判断是否有解,每次钦定一个作为起点的单次询问时间复杂度为 \(O(n^{2})\)

      点击查看代码
      struct node
      {
      	ll nxt,to,w;
      }e[1000010];
      ll head[1000010],u[1000010],v[1000010],w[1000010],b[1000010],sum[1000010],num[2],cnt=0,ans;
      void add(ll u,ll v,ll w)
      {
      	cnt++;
      	e[cnt].nxt=head[u];
      	e[cnt].to=v;
      	e[cnt].w=w;
      	head[u]=cnt;
      }
      void dfs(ll x,ll fa,ll rt,ll w)
      {
      	if(x!=rt)
      	{
      		num[sum[w]%2]--;
      		sum[w]++;
      		num[sum[w]%2]++;
      		ans+=(num[1]>=1);
      	}
      	for(ll i=head[x];i!=0;i=e[i].nxt)
      	{
      		if(e[i].to!=fa)	
      		{
      			dfs(e[i].to,x,rt,e[i].w);
      		}
      	}
      	if(x!=rt)
      	{
      		num[sum[w]%2]--;
      		sum[w]--;
      		num[sum[w]%2]++;
      	}
      }
      int main()
      {
      	int t,n,i,j;
      	cin>>t;
      	for(j=1;j<=t;j++)
      	{
      		cin>>n;
      		ans=cnt=0;
      		memset(e,0,sizeof(e));
      		memset(head,0,sizeof(head));
      		for(i=1;i<=n-1;i++)
      		{
      			cin>>u[i]>>v[i]>>w[i];
      			b[i]=w[i];
      		}
      		sort(b+1,b+n);
      		b[0]=unique(b+1,b+n)-(b+1);
      		for(i=1;i<=n-1;i++)
      		{
      			w[i]=lower_bound(b+1,b+n,w[i])-b;
      			add(u[i],v[i],w[i]);
      			add(v[i],u[i],w[i]);
      		}
      		for(i=1;i<=n;i++)
      		{
      			dfs(i,0,i,0);
      		}
      		cout<<ans/2<<endl;
      	}
      	return 0;
      }
      
  • 正解

    • 发现和哈希在本题中无法合适地处理奇偶性,考虑异或哈希。

    • 具体地,给边权随机赋一个 \([0,2^{64})\) 范围内的值 \(H_{w}\) ,一条路径上边权的集合的哈希值就是其中元素的 \(H\) 值的异或和。

      • rand
        • rand 返回一个 [0,RAND_MAX] 的随机整数。在 \(Linux\)RAND_MAX 等于 \(2^{31}-1\) ,达到了 unsigned int 类型的取值范围;但 WindowsRAND_MAX 等于 \(2^{15}-1\) ,仅达到了 short 的上界。
        • 需要调用 #include<random>
      • mt19937
        • mt19937 作用和随机数范围同 rand() ,均为 unsigned int 类型的取值范围,但随机数质量和速度均比 rand() 优。
        • 需要调用 #include<random>
          • mt19937_64 随机数范围扩大到了 unsigned long long 类型的取值范围。
        • 使用 mt19937 时需先定义一个随机数生成器,例如 mt19937 rng(seed) ,其中随机种子 seed 若不填则为默认随机种子。需要生成随机数时直接调用 rng() 即可。
      • random_device
        • 梅森旋转器 random_device 是一个基于硬件的均匀分布随机数生成器,在 熵池耗尽前 效率较高,耗尽后性能急剧下降,常用作 mt19937 等伪随机数生成器的种子来源。例如 mt19937 rng(random_device{}());
        • 需要调用 #include<random>
    • 另外一种异或哈希的方法。

    • 基于异或的性质,树上差分比较容易,维护树上前缀和后桶维护个数。

    点击查看代码
    struct node
    {
    	ull nxt,to,w;
    }e[1000010];
    ull head[1000010],sum[1000010],cnt=0,ans;
    mt19937_64 rng(random_device{}());
    map<ull,ull>f,g;
    map<ull,ull>::iterator it;
    void add(ull u,ull v,ull w)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	e[cnt].w=w;
    	head[u]=cnt;
    }
    void dfs(ull x,ull fa,ull w)
    {
    	sum[x]=sum[fa]^w;
    	ans-=g[sum[x]];
    	g[sum[x]]++;
    	for(ull i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x,e[i].w);
    		}
    	}
    }
    int main()
    {
    	ull t,n,u,v,w,i,j;
    	cin>>t;
    	for(j=1;j<=t;j++)
    	{
    		cin>>n;
    		ans=n*(n-1)/2;
    		cnt=0;
    		memset(e,0,sizeof(e));
    		memset(head,0,sizeof(head));
    		g.clear();
    		for(i=1;i<=n-1;i++)
    		{
    			cin>>u>>v>>w;
    			if(f.find(w)==f.end())
    			{
    				f[w]=rng();
    			}
    			add(u,v,f[w]);
    			add(v,u,f[w]);
    		}
    		dfs(1,0,0);
    		cout<<ans<<endl;
    	}
    	return 0;
    }
    

\(T2\) P274. 跳跃 \(20pts\)

  • 原题: 2022牛客OI赛前集训营-提高组(第四场) B 跳跃

  • 部分分

    • \(20pts\) :爆搜。

      点击查看代码
      ll a[1010],sum[1010],ans;
      void dfs(ll pos,ll x,ll num,ll n,ll k)
      {
      	ans=max(ans,num);
      	if(pos==k+1)
      	{
      		return;
      	}
      	else
      	{
      		if(pos&1)
      		{
      			for(ll i=x;i<=n;i++)
      			{
      				if(num+sum[i]-sum[x-1]>=0)
      				{
      					dfs(pos+1,i,num+sum[i]-sum[x-1],n,k);
      				}
      			}
      		}
      		else
      		{
      			for(ll i=1;i<=x;i++)
      			{
      				if(num+sum[x]-sum[i-1]>=0)
      				{
      					dfs(pos+1,i,num+sum[x]-sum[i-1],n,k);
      				}
      			}
      		}
      	}
      }
      int main()
      {
      	ll t,n,k,i,j;
      	scanf("%lld",&t);
      	for(j=1;j<=t;j++)
      	{
      		scanf("%lld%lld",&n,&k);
      		ans=0;
      		for(i=1;i<=n;i++)
      		{
      			scanf("%lld",&a[i]);
      			sum[i]=sum[i-1]+a[i];
      		}
      		dfs(1,1,0,n,k);
      		printf("%lld\n",ans);
      	}
      	return 0;
      }
      
    • \(f_{i,j}\) 表示从 \(1\) 跳到 \(i\) 一共跳了 \(j\) 步的最大得分,状态转移方程为 \(f_{i,j}=\begin{cases} \max\limits_{k=1}^{i} \{ f_{k,j-1}+\sum\limits_{h=k}^{i}a_{h} \} & j \bmod 2=0 \\ \max\limits_{k=i}^{n} \{ f_{k,j-1}+\sum\limits_{h=i}^{k}a_{h} \} & j \bmod 2=1 \end{cases}\) ,维护前/后缀 \(\max\) 后时间复杂度为 \(O(nk)\)

  • 正解

    • 不难想到,我们会在一些连续段反复横跳,使得能跳到下一个更优的连续段继续反复横跳。
    • 处理出 \(suf_{i}=\max\limits_{j=1}^{i}\{ \sum\limits_{k=j}^{i}a_{k} \}\)
    • \(f_{i}\) 表示从 \(1\) 跳到 \(i\) 后至多还能跳几次, \(g_{i}\) 表示在前者取到最优时从 \(1\) 跳到 \(i\) 时的最大得分。
    • 转移时枚举上一步 \(j\)\(suf_{j}\) 可以方便地计算出跳到 \(i\) 的最小次数。
    • 最后统计答案时枚举所有右端点进行左右横跳即可。
    点击查看代码
    ll f[1010],g[1000],a[1010],sum[1010],suf[1010];
    int main()
    {
    	ll t,n,k,ans,minn,d,cnt,num,i,j,h;
    	cin>>t;
    	for(h=1;h<=t;h++)
    	{
    		cin>>n>>k;
    		ans=minn=0;
    		for(i=1;i<=n;i++)
    		{
    			cin>>a[i];
    			sum[i]=sum[i-1]+a[i];
    			suf[i]=sum[i]-minn;
    			minn=min(minn,sum[i]);
    			if(sum[i]>=0)
    			{
    				f[i]=k-1;
    				g[i]=sum[i];
    			}
    			else
    			{
    				f[i]=-1;
    				g[i]=-0x7f7f7f7f;
    			}
    		}
    		for(i=1;i<=n;i++)
    		{
    			for(j=i+1;j<=n;j++)
    			{
    				if(suf[j]>=suf[i])
    				{
    					d=sum[j]-sum[i];
    					if(g[i]+d<0)
    					{
    						if(suf[i]>0)
    						{
    							cnt=ceil((0-(g[i]+d))/(2.0*suf[i]));
    							num=g[i]+d+2*cnt*suf[i];
    							if(f[j]<f[i]-2*cnt)
    							{
    								f[j]=f[i]-2*cnt;
    								g[j]=num;
    							}
    							else
    							{	
    								if(f[j]==f[i]-2*cnt)
    								{
    									g[j]=max(g[j],num);
    								}
    							}
    						}
    					}
    					else
    					{
    						if(f[j]<f[i]-(i==1))
    						{
    							f[j]=f[i]-(i==1);
    							g[j]=g[i]+d;
    						}
    						else
    						{
    							if(f[j]==f[i]-(i==1))
    							{
    								g[j]=max(g[j],g[i]+d);
    							}
    						}
    					}
    				}
    			}			
    		}
    		for(i=1;i<=n;i++)
    		{
    			ans=max(ans,f[i]*suf[i]+g[i]);
    		}
    		cout<<ans<<endl;
    	}
    	return 0;
    }
    

\(T3\) P275. 大陆 \(0pts\)

  • 原题: 2022牛客OI赛前集训营-提高组(第四场) C 大陆 | luogu P2325 [SCOI2005] 王室联邦

  • 貌似是树分块的前置知识。

  • 考虑对整棵子树进行 \(DFS\) ,处理每个节点时先将部分子节点分块,未被分块的子节点回溯到上一层。

  • 具体地,枚举 \(x\) 的所有子节点 \(y\) ,递归处理完子树后,将未被分块的子节点加入待选集合 \(S\) 中,途中一旦 \(|S| \ge b\) 就将 \(S\) 作为一个新的块(块大小至多为 \(2b-2\) )并以 \(x\) 作为省会,并清空 \(S\) 。接着加入 \(x\) ,此时 \(|S|\) 至多为 \(b-1+1=b\)

  • 最后把集合 \(S\) 中剩余的节点加入最后一个块,块大小至多为 \(2b-2+b+1=3b-1\) ,符合题意。

  • 特判只有一个节点的情况。

    点击查看代码
    struct node
    {
    	int nxt,to;
    }e[20010];
    int head[20010],pos[20010],rt[20010],cnt=0,ksum=0;
    stack<int>s;
    void add(int u,int v)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	head[u]=cnt;
    }
    void dfs(int x,int fa,int b)
    {
    	int tmp=s.size();
    	for(int i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x,b);
    			if(s.size()-tmp>=b)
    			{
    				ksum++;
    				rt[ksum]=x;
    				while(s.size()>tmp)
    				{
    					pos[s.top()]=ksum;
    					s.pop();
    				}
    			}
    		}
    	}
    	s.push(x);
    }
    int main()
    {
    	int n,b,u,v,i;
    	cin>>n>>b;
    	for(i=1;i<=n-1;i++)
    	{
    		cin>>u>>v;
    		add(u,v);
    		add(v,u);
    	}
    	dfs(1,0,b);
    	if(ksum==0)
    	{
    		ksum++;
    		rt[ksum]=1;
    	}
    	while(s.empty()==0)
    	{
    		pos[s.top()]=ksum;
    		s.pop();
    	}
    	cout<<ksum<<endl;
    	for(i=1;i<=n;i++)
    	{
    		cout<<pos[i]<<" ";
    	}
    	cout<<endl;
    	for(i=1;i<=ksum;i++)
    	{
    		cout<<rt[i]<<" ";
    	}
    	cout<<endl;
    	return 0;
    }
    

\(T4\) P276. 排列 \(40pts\)

  • 原题: 2022牛客OI赛前集训营-提高组(第四场) D 排列 | Baekjoon 17961.수열과 쿼리 35
  • 部分分
    • \(40pts\) :暴力进行循环右移,权值树状数组重构整个序列,时间复杂度为 \(O(qn \log n)\)

      点击查看代码
      int a[120010],b[120010];
      struct BIT
      {
      	int c[120010];
      	int lowbit(int x)
      	{
      		return (x&(-x));
      	}
      	void init(int n)
      	{
      		for(int i=1;i<=n;i++)
      		{
      			c[i]=0;
      		}
      	}
      	void add(int n,int x,int val)
      	{
      		for(int i=x;i<=n;i+=lowbit(i))
      		{
      			c[i]+=val;
      		}
      	}
      	int getsum(int x)
      	{
      		int ans=0;
      		for(int i=x;i>=1;i-=lowbit(i))
      		{
      			ans+=c[i];
      		}
      		return ans;
      	}
      }le,ri;
      bool ask(int l,int r,int k,int n)
      {
      	for(int i=l;i<=r;i++)
      	{
      		b[i]=a[i];
      	}
      	for(int i=l,j=r-k+1;i<=l+k-1;i++,j++)
      	{
      		a[i]=b[j];
      	}
      	for(int i=l+k,j=l;i<=r;i++,j++)
      	{
      		a[i]=b[j];
      	}
      	le.init(n);
      	ri.init(n);
      	for(int i=1;i<=n;i++)
      	{
      		ri.add(n,a[i],1);
      	}
      	for(int i=1;i<=n;i++)
      	{
      		ri.add(n,a[i],-1);
      		if(le.getsum(a[i]-1)>=1&&ri.getsum(n)-ri.getsum(a[i])>=1)
      		{
      			return true;
      		}
      		le.add(n,a[i],1);
      	}
      	return false;
      }
      int main()
      {
      	int n,m,l,r,k,i;
      	cin>>n;
      	for(i=1;i<=n;i++)
      	{
      		cin>>a[i];
      	}
      	cin>>m;
      	for(i=1;i<=m;i++)
      	{
      		cin>>l>>r>>k;
      		if(ask(l,r,k,n)==true)
      		{
      			cout<<"YES"<<endl;
      		}
      		else
      		{
      			cout<<"NO"<<endl;
      		}
      	}
      	return 0;
      }
      
  • 正解
    • 涉及到对 \(FHQ-Treap\) 的应用和理解,挂个官方题解就跑路了。

总结

  • \(T1\)\(VScode\) 终端编译延迟导致显示的是上份代码的信息,以为是这份代码的信息,然后没看出是 \(CE\) ,挂了 \(30pts\)

后记

  • 组题人对 \(T1',T2',T3',T4'\) 的评价。

  • \(T2',T3'\) 我们都打过,所以 \(miaomiao\) 干脆把原来的 \(4\) 道题都换了。原 \(7:00\) 开始的比赛改成了 \(7:10\) 开始。

posted @ 2024-08-21 15:12  hzoi_Shadow  阅读(116)  评论(11编辑  收藏  举报
扩大
缩小