暑假集训CSP提高模拟5

暑假集训CSP提高模拟5

组题人: @worldvanquisher

\(T1\) P140. 简单的序列(sequence) \(30pts\)

  • 原题: CF1675B Make It Increasing

  • 部分分

    • \(30pts\)
      • 算法流程

        • \(1 \sim n\) 中找到一个最大的数使得它目前所处的位置和排序后所处的位置不同或目前所处的位置和排序后所处的位置相同但和右边的数相等,将这个数进行一次操作。

        • 手摸样例如下

          点击查看样例
          6:
          8 26 5 21 10
          8 13 5 21 10
          8 13 5 10 10
          8 6 5 10 10
          8 6 5 5 10
          4 6 5 5 10
          4 3 5 5 10
          4 3 2 5 10
          2 3 2 5 10
          2 1 2 5 10
          1 1 2 5 10
          0 1 2 5 10
          ____________
          5:
          2 8 7 5
          2 4 7 5
          2 4 3 5
          2 2 3 5
          1 2 3 5 
          
        点击查看代码
        struct node
        {
        	int val,pos;
        }a[100];
        bool cmp(node a,node b)
        {
        	return (a.val==b.val)?(a.pos<b.pos):(a.val<b.val);
        }
        int main()
        {
        	int t,n,flag,ans,i,j;
        	scanf("%d",&t);
        	for(j=1;j<=t;j++)
        	{
        		scanf("%d",&n);
        		flag=ans=0;
        		memset(a,-0x3f,sizeof(a));
        		for(i=1;i<=n;i++)
        		{
        			scanf("%d",&a[i].val);
        			a[i].pos=i;
        			flag|=(a[i].val==1&&i>=3);
        		}
        		if(flag==1)
        		{
        			ans=-1;
        		}
        		else
        		{
        			while(flag==0)
        			{   
        				flag=1;
        				sort(a+1,a+1+n,cmp);
        				for(i=n;i>=1;i--)
        				{
        					if(a[i].pos!=i||a[i+1].val==a[i].val)
        					{
        						a[i].val/=2;
        						ans++;
        						flag=0;
        						break;
        					}
        				}
        			}
        		}
        		printf("%d\n",ans);
        	}
        	return 0;
        }
        
  • 正解

    • 观察到 \(a_{i}\) 会对 \(i\) 前后的数产生影响。但小的数不会变大,只能让大的数变小。
    • 故考虑贪心,从后往前遍历,若不小于后面的数则除以 \(2\) ,特判 \(0\)
    点击查看代码
    int a[100];
    int main()
    {
    	int t,n,ans,i,j;
    	cin>>t;
    	for(j=1;j<=t;j++)
    	{
    		cin>>n;
    		ans=0;
    		for(i=1;i<=n;i++)
    		{
    			cin>>a[i];
    		}
    		for(i=n-1;i>=1;i--)
    		{
    			while(a[i]>=a[i+1]&&a[i]!=0&&a[i+1]!=0)
    			{
    				a[i]/=2;
    				ans++;
    			}
    			if(a[i]>=a[i+1])
    			{
    				ans=-1;
    				break;
    			}
    		}
    		cout<<ans<<endl;
    	}
    	return 0;
    }
    

\(T2\) P141. 简单的字符串(string) \(100pts\)

  • 原题: [AGC016A] Shrinking

  • 最小操作次数实际上就是缩短了几位。

  • 设当前是第 \(k\) 次操作,则 \(s_{i}\) 可能的取值仅有 \(s_{i},s_{i+1},s_{i+2} ,\dots ,s_{i+k}\)

  • 部分分

    • \(100pts\)
      • 枚举 \(x\) 依次判断,双指针维护即可,时间复杂度为 \(O(n^{3})\)

        • 随机数据下跑得挺快,但造一堆连续相同字符的字符串就卡掉了。
        点击查看代码
        char s[10010];
        int main()
        {
        	int n,flag=0,i,j,k,h;
        	cin>>(s+1);
        	n=strlen(s+1);
        	for(i=n;i>=1;i--)
        	{
        		for(h=i;h<=n;h++)
        		{
        			for(j=1;j<=i-1;j++)
        			{
        				flag=0;
        				for(k=j;k<=j+n-i;k++)
        				{
        					if(s[k]==s[h])
        					{
        						j=k;
        						flag=1;
        						break;
        					}
        				}
        				if(flag==0)
        				{
        					break;  
        				}
        			}
        			if(flag==1)
        			{
        				cout<<n-i<<endl;
        				return 0;
        			}
        		}
        	}
        	cout<<n-1<<endl;//长度 <3 的要特殊处理
        	return 0;
        }
        
      • 用前缀和优化查找过程,用 bitset 压一下使时间复杂度为 \(O(\frac{n^{2}|\sum|}{w})\) ,其中 \(|\sum|\) 取字符集大小 \(26\)

        点击查看代码
        int sum[10010];
        char s[10010];
        bitset<30>vis;
        int val(char x)
        {
        	return x-'a'+1;
        }
        int main()
        {
        	int n,flag=0,i,j,k;
        	cin>>(s+1);
        	n=strlen(s+1);
        	for(k=0;k<=n;k++)
        	{
        		vis[val(s[n-k])]=1;
        		for(j='a';j<='z';j++)
        		{
        			if(vis[val(j)]==1)
        			{
        				flag=1;
        				for(i=1;i<=n;i++)
        				{
        					sum[i]=sum[i-1]+(s[i]==j);
        				}
        				for(i=1;i<=n-k;i++)
        				{
        					flag&=(sum[i+k]-sum[i-1]>=1);
        				}
        				if(flag==1)
        				{
        					cout<<k<<endl;
        					return 0;
        				}
        			}
        		}
        	}
        	return 0;
        }
        
  • 正解

    • \(s_{i}\) 的变化过程视作 \(s_{i},s_{i+1},s_{i+2} ,\dots ,s_{i+k}\) 不断向左覆盖的过程,时间复杂度为 \(O(n|\sum|)\) ,其中 \(|\sum|\) 取字符集大小 \(26\)
    点击查看代码
    char s[10010];
    int main()
    {
    	int n,ans=0x3f3f3f3f,len,pos,i,j;
    	scanf("%s",s+1);
    	n=strlen(s+1);
    	for(i='a';i<='z';i++)
    	{
    		len=-0x3f3f3f3f;
    		pos=0;
    		s[n+1]=i;
    		for(j=1;j<=n+1;j++)
    		{
    			if(s[j]==s[n+1])
    			{
    				len=max(len,j-pos-1);//计算非 i 的最长段
    				pos=j;
    			}
    		}
    		if(len!=-0x3f3f3f3f)
    		{
    			ans=min(ans,len);
    		}
    	}
    	printf("%d\n",ans);
    	return 0;
    }
    

\(T3\) P142. 简单的博弈(tree) \(50pts\)

  • 原题: [AGC017D] Game on Tree

  • 部分分

    • \(50pts\) :随机的三个点和菊花的两个点,瞎口胡结论或 rand() 即可。
  • 正解

    • \(f_{x}\) 表示以 \(x\) 为根的子树的 \(SG\) 函数值(子节点不能走到的最小状态),状态转移方程为 \(f_{x} =\bigoplus\limits_{y \in Son(x)}(f_{y}+1)\) ,边界为叶子节点的 \(SG\) 函数值为零。
      • 证明
        • \(x\)\(k\) 个子节点 \(\{ son_{x} \}\) ,那么可以将分成 \(k\) 个独立的部分,每个部分包括 \(x\)\(x\) 的一个子节点。
        • 问题来到了怎么求只有一个儿子的 \(SG\) 函数。当 \(son_{x,i}\) 为叶子节点时只能断掉 \((x,son_{x,i})\) 这条边,次数有 \(SG(x)=1\) ;当 \(son_{x,i}\) 不为叶子节点时,可以选择断掉 \((x,son_{x,i})\) 到达 \(0\) 的状态,也可以选择断掉 \(son_{x,i}\) 子树内的边到达 \(son_{x,i}\) 子树割掉这条边后的 \(SG\) 函数值加一,故有 \(SG(x)=SG(son_{x,i})+1\)
        • 接着将这 \(k\) 个部分的值异或起来即得到了 \(f_{x}\)
    • 最后由于 \(SG\) 定理,若 \(f_{1}>0\) 则先手必胜,若 \(f_{1}=0\) 则后手必胜。
    点击查看代码
    struct node
    {
    	int nxt,to;
    }e[400010];
    int head[400010],sg[400010],cnt=0;
    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)
    {
    	for(int i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x);
    			sg[x]^=sg[e[i].to]+1;
    		}
    	}
    }
    int main()
    {
    	int t,n,u,v,i,j;
    	cin>>t;
    	for(j=1;j<=t;j++)
    	{
    		cin>>n;
    		cnt=0;
    		memset(e,0,sizeof(e));
    		memset(head,0,sizeof(head));
    		memset(sg,0,sizeof(sg));
    		for(i=1;i<=n-1;i++)
    		{
    			cin>>u>>v;
    			add(u,v);
    			add(v,u);
    		}
    		dfs(1,0);
    		if(sg[1]==0)
    		{
    			cout<<"joke3579"<<endl;
    		}
    		else
    		{
    			cout<<"gtm1514"<<endl;
    		}
    	}
    	return 0;
    }
    

\(T4\) P143. 困难的图论(graph) \(0pts\)

  • 原题: UOJ 605. 【UER #9】知识网络

  • \(p \to q\) 的路径序列长度 \(x\) 视作在边权为 \(1\) 的情况下 \(p \to q\) 的最短路长度加一,即 \(\forall 1 \le p<q \le n,f(p,q) \ge 2\)

  • 部分分

    • \(30pts\) :暴力建边,每个类别建一个虚点,同一个类别内部的点到虚点的边权为 \(1\) ,虚点到这些点的边权为 \(0\) 。因为边权只有 \(0\)\(1\) ,跑 \(01BFS\) 即可。
    • \(50pts\) :观察到 \(m \le 3000\) ,此时图大部分是不连通的,那么就完全不需要管没被这 \(m\) 条边覆盖的点,因为这些点的答案可以通过虚点计算。对这 \(m\) 条边和虚点跑最短路。
  • 正解

    • 考虑对每个类别以虚点为起点,跑最短路并建出最短路 \(DAG\) ,那么此时 \(p \to q\) 的路径序列要么经过虚点要么不经过虚点,若经过 \(p\) 所属的类别的虚点则虚点的答案可以直接继承给 \(p\) ,若不过 \(p\) 所属的类别的虚点则多走 \(1\) 步走到虚点。
    • 枚举终点,用 bitset 用来记录下前驱节点跑拓扑转移。
    • 需要手写 bitset\(UOJ\) 上因为空间限制开到了 \(256MB\) ,需要对其进行分块,用 unsigned long long\(64\) 位。
    点击查看代码
    struct node
    {
    	ll nxt,to,w;
    }e[200010];
    ll head[200010],p[200010],dis[200010],din[200010],dinn[200010],ans[200010],cnt=0;
    ull pre[200010];
    vector<ll>pos[160],E[200010];
    void add(int u,int v,int w)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	e[cnt].w=w;
    	head[u]=cnt;
    }
    void bfs(ll s)
    {
    	ll x,i;
    	deque<ll>q;
    	memset(dis,0x3f,sizeof(dis));
    	dis[s]=0;
    	q.push_back(s);
    	while(q.empty()==0)
    	{
    		x=q.front();
    		q.pop_front();
    		for(i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(dis[e[i].to]>dis[x]+e[i].w)
    			{
    				dis[e[i].to]=dis[x]+e[i].w;
    				if(e[i].w==1)
    				{
    					q.push_back(e[i].to);
    				}
    				else
    				{
    					q.push_front(e[i].to);
    				}
    			}
    		}
    	}
    }
    void build(ll n)
    {
    	memset(din,0,sizeof(din));
    	for(ll x=1;x<=n;x++)
    	{
    		E[x].clear();
    		for(ll i=head[x];i!=0;i=e[i].nxt)
    		{
    			if(dis[e[i].to]==dis[x]+e[i].w)
    			{
    				E[x].push_back(e[i].to);
    				din[e[i].to]++;
    			}
    		}
    	}
    }
    void top_sort(ll n)
    {
    	ll x,i;
    	queue<ll>q;
    	for(ll i=1;i<=n;i++)
    	{
    		dinn[i]=din[i];
    		if(dinn[i]==0)
    		{
    			q.push(i);
    		}
    	}   
    	while(q.empty()==0)
    	{
    		x=q.front();
    		q.pop();
    		for(i=0;i<E[x].size();i++)
    		{
    			pre[E[x][i]]|=pre[x];
    			dinn[E[x][i]]--;
    			if(dinn[E[x][i]]==0)
    			{
    				q.push(E[x][i]);
    			}
    		}
    	}
    }
    int main()
    {
    	ll n,m,k,siz,u,v,w,i,l,r,j;
    	cin>>n>>m>>k;
    	for(i=1;i<=n;i++)
    	{
    		cin>>p[i];
    		pos[p[i]].push_back(i);
    		add(i,n+p[i],1);
    		add(n+p[i],i,0);
    	}
    	for(i=1;i<=m;i++)
    	{
    		cin>>u>>v;
    		w=1;
    		add(u,v,w);
    		add(v,u,w);
    	}
    	for(i=1;i<=k;i++)
    	{
    		siz=pos[i].size();
    		bfs(n+i);
    		build(n+k);
    		for(l=0,r=min(l+63,siz-1);l<=siz-1;l=r+1,r=min(l+63,siz-1))
    		{
    			memset(pre,0,sizeof(pre));
    			for(j=l;j<=r;j++)
    			{
    				pre[pos[i][j]]|=(1ull<<(j-l));//自己能到自己
    			}
    			top_sort(n+k);
    			for(j=1;j<=n;j++)
    			{
    				if(dis[j]==0x3f3f3f3f3f3f3f3f)
    				{
    					ans[2*k+1]+=r-l+1;
    				}
    				else
    				{
    					ans[dis[j]+1]+=__builtin_popcountll(pre[j]);//走到虚点,直接继承(最短路长度不够的走这个类别的点到达)
    					ans[dis[j]+1+1]+=r-l+1-__builtin_popcountll(pre[j]);//不走到虚点,多走 1 步到虚点
    				}
    			}
    		}   
    	}
    	ans[1]=0;
    	for(i=1;i<=2*k+1;i++)
    	{
    		cout<<ans[i]/2<<" ";//因为是每一个种类算一遍,所以 (p,q) 算了两遍,需要除以 2 
    	}
    	return 0;
    }
    

总结

  • \(T1\) 多写了个无用变量,从 \(50pts\) 挂到了 \(30pts\)
  • \(T3\) @worldvanquisher 称是用来普及 \(SG\) 函数的。
  • \(T4\) 读假题了,导致没写暴力,挂了 \(10pts/30pts\)

后记

  • \(T1\) 没标明 \(\{ a \}\) 不为负数。

  • \(T2\) 没标明 \(|\sum|\) 大小,没解释操作具体啥意思,数据随机。

  • \(T3\) 大样例又错了,中途更换。

  • 首尾呼应

posted @ 2024-07-23 11:01  hzoi_Shadow  阅读(57)  评论(0编辑  收藏  举报
扩大
缩小