暑假集训CSP提高模拟7

暑假集训CSP提高模拟7

组题人: @KafuuChinocpp | @Chen_jr

\(T1\) P122. Permutations & Primes \(20pts\)

  • 原题: CF1844B Permutations & Primes

  • 假的构造策略,拿到了 \(20pts\)

    • \(n\) 为奇数,则将 \(1\) 放在 \(\left\lceil \frac{n}{2} \right\rceil\) 的位置上,前一半质数降序放置,如果数量不够则合数降序放置,后一半降序放置。
    • \(n\) 为偶数,则将 \(1\) 放在 \(\frac{n}{2}\) 的位置上,前一半质数降序放置,如果数量不够则合数降序放置,后一半降序放置。
  • 正解

    • 观察到 \(1\) 对整个 \(\operatorname{mex}\) 的影响较大,故让包含 \(1\) 的区间尽量多,由 普及模拟1 T1 Past 的结论当取 \(i=n-i+1\) 时取到最大值,把 \(1\) 放在 \(\left\lceil \frac{n}{2} \right\rceil\) 的位置,然后将 \(2,3\) 分别放在首尾,剩下的随便填即可。
    点击查看代码
    int main()
    {
        ll t,n,i,j,k;
        cin>>t;
        for(j=1;j<=t;j++)
        {
            cin>>n;
            if(n==1)
            {
                cout<<"1"<<endl;
            }
            else
            {
                if(n==2)
                {
                    cout<<"2 1"<<endl;
                }
                else
                {
                    cout<<"2 ";
                    for(i=2,k=4;i<=n/2;i++,k++)
                    {
                        cout<<k<<" ";
                    }
                    cout<<"1 ";
                    for(i=n/2+2;i<=n-1;i++,k++)
                    {
                        cout<<k  <<" ";
                    }
                    cout<<"3"<<endl;
                }
            }
        }
        return 0;
    }
    

\(T2\) P135. 树上游戏 \(29pts\)

  • 原题: [ARC116E] Spread of Information | luogu P3523 [POI2011] DYN-Dynamite

  • 部分分

    • \(10pts\) :当 \(k=n-1\) 时输出 \(1\)
    • \(10pts\) :当 \(k=1\) 时求出树的直径除以 \(2\) 并向上取整即可。
    • 随机 \(pts\)rand() 大法好。
  • 正解

    • 答案显然具有单调性,考虑二分答案。
    • 设当前二分出的答案为 \(mid\) ,则等价于覆盖距离为 \(mid\) 的情况下进行选点。
    • 做法同 luogu P3942 将军令 ,考虑进行贪心,对于深度最深的叶节点将选择的点放在边界时,即取 \(mid\) 级祖先时,覆盖的范围一定最大。
    • \(f_{x}\) 表示 \(x\) 到以 \(x\) 为根的子树内最远的没被覆盖的点的距离, \(g_{x}\) 表示 \(x\) 到以 \(x\) 为根的子树内最近被选的点的距离,状态转移方程为 \(\begin{cases} f_{x}=\max\limits_{y \in Son(x)}\{ f_{y}+1 \} \\ g_{x}=\min\limits_{y \in Son(x)}\{ g_{y}+1 \}\end{cases}\)
    • \(g_{x}>mid\) 时以 \(x\) 为根的子树内所选的点覆盖不到自己,需要祖先节点进行覆盖,此时需要统计自己的贡献,即 \(f_{x}=\max(f_{x},0)\) ;当 \(f_{x}+g_{x} \le mid\) 时以 \(x\) 为根的子树内所选的点就能覆盖整棵子树,令 \(f_{x}=- \infty\) ;当 \(f_{x}=mid\) 说明 \(x\) 必须被选,令 \(f_{x}=- \infty,g_{x}=0\) ,选择点数加一。
    • 特判下根节点没有被覆盖的情况。
    点击查看代码
    struct node
    {
        int nxt,to;
    }e[400010];
    int head[400010],f[400010],g[400010],cnt=0,sum=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,int k)
    {
        f[x]=-0x3f3f3f3f;
        g[x]=0x3f3f3f3f;
        for(int i=head[x];i!=0;i=e[i].nxt)
        {
            if(e[i].to!=fa)
            {
                dfs(e[i].to,x,k);
                f[x]=max(f[x],f[e[i].to]+1);
                g[x]=min(g[x],g[e[i].to]+1);
            }
        }
        if(g[x]>k)
        {
            f[x]=max(f[x],0);//等待祖先的覆盖
        }
        if(f[x]+g[x]<=k)//已被覆盖
        {
            f[x]=-0x3f3f3f3f;
        }
        if(f[x]==k)//强制覆盖
        {
            f[x]=-0x3f3f3f3f;
            g[x]=0;
            sum++;
        }
    }
    bool check(int mid,int k)
    {
        sum=0;
        dfs(1,0,mid);
        sum+=(f[1]>=0);//自己到自己也得算
        return sum<=k;	
    }
    int main()
    {
        int n,k,u,v,l=0,r,mid,ans=0,i;
        cin>>n>>k;
        r=n;
        for(i=1;i<=n-1;i++)
        {
            cin>>u>>v;
            add(u,v);
            add(v,u);
        }
        while(l<=r)
        {
            mid=(l+r)/2;
            if(check(mid,k)==true)
            {
                ans=mid;
                r=mid-1;
            }
            else
            {
                l=mid+1;
            }
        }
        cout<<ans<<endl;
        return 0;
    }
    

\(T3\) P127. Ball Collector \(0pts\)

  • 原题: [ABC302Ex] Ball Collector

  • 部分分

    • \(20pts\) :将选 \(a_{i}/b_{i}\) 进行状态压缩判断。
  • 正解

    • 对于每个点 \(i\) ,从 \(a_{i}\)\(b_{i}\) 连一条无向边。
    • 对于图中的每个连通块单独考虑。
    • 设此时遍历的连通块 \(G=(V,E)\) ,则这个连通块对答案产生的贡献为 \(\min(V,E)\)
      • 由于是连通块,所以 \(E \ge V-1\)
      • \(E=V-1\) 时这个连通块是一棵树,至多选 \(E=V-1\) 个点。
      • \(E \le V\) 时每个点一定能被至少分到一条边使其被选到,至多选 \(V\) 个点。
    • 并查集维护连通块及内部点数、边数。
    • 由于需要遍历每一个节点,所以在回溯过程中需要撤销不在路径上的点影响,使用可撤销并查集维护即可。
      • 可撤销并查集只能按秩合并,路径压缩的话会改变树的形态导致无法撤销。
      • 合并前栈记录下原值,撤销时按照栈内的原值更新回去。
    点击查看代码
    struct node
    {
    	int nxt,to;
    }e[400010];
    struct quality
    {
    	int id,fa,siz,edge;
    };
    int head[400010],a[400010],b[400010],fa[400010],siz[400010],edge[400010],ans[400010],cnt=0,sum=0;
    void add(int u,int v)
    {
    	cnt++;
    	e[cnt].nxt=head[u];
    	e[cnt].to=v;
    	head[u]=cnt;
    }
    int find(int x)
    {
    	return (fa[x]==x)?x:find(fa[x]);
    }
    void merge(int x,int y,stack<quality> &s,int &sum)
    {	
    	x=find(x);
    	y=find(y);
    	s.push((quality){x,fa[x],siz[x],edge[x]});
    	s.push((quality){y,fa[y],siz[y],edge[y]});
    	if(x==y)//自己到自己需要特判
    	{
    		sum-=min(siz[x],edge[x]);
    		edge[x]++;
    		sum+=min(siz[x],edge[x]);
    	}
    	else
    	{
    		if(siz[x]<siz[y])
    		{
    			swap(x,y);
    		}
    		sum-=min(siz[x],edge[x]);
    		sum-=min(siz[y],edge[y]);
    		fa[y]=x;
    		siz[x]+=siz[y];
    		edge[x]+=edge[y]+1;//连一条边导致多了一条边
    		sum+=min(siz[x],edge[x]);
    	}
    }
    void split(stack<quality>&s)
    {
    	while(s.empty()==0)//撤销
    	{
    		fa[s.top().id]=s.top().fa;
    		siz[s.top().id]=s.top().siz;
    		edge[s.top().id]=s.top().edge;
    		s.pop();
    	}
    }
    void dfs(int x,int fa)
    {
    	stack<quality>s;
    	int last=sum;
    	merge(a[x],b[x],s,sum);
    	ans[x]=sum;
    	for(int i=head[x];i!=0;i=e[i].nxt)
    	{
    		if(e[i].to!=fa)
    		{
    			dfs(e[i].to,x);
    		}
    	}
    	split(s);
    	sum=last;//同样也需要撤销
    }
    int main()
    {
    	int n,u,v,i;
    	cin>>n;
    	for(i=1;i<=n;i++)
    	{
    		cin>>a[i]>>b[i];
    		fa[i]=i;
    		siz[i]=1;
    		edge[i]=0;
    	}
    	for(i=1;i<=n-1;i++)
    	{
    		cin>>u>>v;
    		add(u,v);
    		add(v,u);
    	}
    	dfs(1,0);
    	for(i=2;i<=n;i++)
    	{
    		cout<<ans[i]<<" ";
    	}
    	return 0;
    }
    

\(T4\) P159. 满穗 \(20pts\)

  • 原题: luogu P7028 [NWRRC2017] Joker

  • 部分分

    • \(20pts\) :暴力修改, @wkh2008\(hack\) 数据能卡精度,需要写 eps

      点击查看 hack 数据
      in:
      5 0 
      2 -3 2 1 -2
      
      ans:
      1
      
      
      点击查看代码
      const double eps=1e-10;
      double a[50010];
      int main()
      {
      	ll n,m,p=0,q=0,pos=0,x,y,i,j;
      	double sum=0,ans=-0x7f7f7f7f;
      	cin>>n>>m;
      	for(i=1;i<=n;i++)
      	{
      		cin>>a[i];
      		p+=(a[i]>=0)*a[i];
      		q+=(a[i]<0)*a[i];
      	}
      	for(i=1;i<=n;i++)
      	{
      		sum+=a[i]/((a[i]>=0)?p:-q);
      		if(sum-ans>eps)
      		{
      			ans=sum;
      			pos=i;
      		}
      	}
      	cout<<pos<<endl;
      	for(j=1;j<=m;j++)
      	{
      		cin>>x>>y;
      		p-=(a[x]>=0)*a[x];
      		q-=(a[x]<0)*a[x];
      		a[x]=y;
      		p+=(a[x]>=0)*a[x];
      		q+=(a[x]<0)*a[x];
      		pos=sum=0;
      		ans=-0x7f7f7f7f;
      		for(i=1;i<=n;i++)
      		{
      			sum+=a[i]/((a[i]>=0)?p:-q);
      			if(sum-ans>eps)
      			{
      				ans=sum;
      				pos=i;
      			}
      		}
      		cout<<pos<<endl;
      	}
      	return 0;
      }
      
  • 正解

    • \(\begin{cases} p_{i}=\sum\limits_{j=1}^{i}[a_{i} \ge 0] \times a_{i} \\ q_{i}=\sum\limits_{j=1}^{i}[a_{i}<0] \times a_{i} \end{cases}\) ,此时 \(s_{i}=\dfrac{p_{i}}{P}-\dfrac{q_{i}}{Q}\)
    • \(i<j,s_{i}<s_{j}\) 则有 \(\dfrac{p_{i}}{P}-\dfrac{q_{i}}{Q}<\dfrac{p_{j}}{P}-\dfrac{q_{j}}{Q}\) ,移项有 \(\dfrac{p_{i}-p_{j}}{P}<\dfrac{q_{i}-q_{j}}{Q}\) ,又因为 \(p_{i}-p_{j} \le 0,Q<0\)\(\dfrac{Q}{P} < \dfrac{q_{i}-q_{j}}{p_{i}-p_{j}}\) ,即 \(Q(p_{i}-p_{j})>P(q_{i}-q_{j})\)
    • 发现很像斜率优化的式子,以 \(\{ p \}\) 为横坐标,以 \(\{ q \}\) 为纵坐标建立平面直角坐标系,其中横坐标 \(\{ p \}\) 是单调不降的,横坐标 \(\{ p \}\) 是单调不增的。
    • 由于斜率 \(\dfrac{Q}{P}<0\) ,考虑维护上凸包相邻两点斜率 \(>\dfrac{Q}{P}\) 的部分,二分队列维护。
    • 难点来自修改。设当前修改位置为 \(i\) ,则对于 \(j \ne i\) 的位置均有它们的凸包仅发生了平移,斜率不变(因为相互抵消了)。
    • 考虑对序列分块,每个块内单独建出凸包,修改时将所属的块的凸包重构,其他凸包平移;查询时二分整个凸包取 \(\max\)
    • 块长取 \(\sqrt{n \log{n}}\)
    点击查看代码
    const double eps=1e-15;
    ll a[50010],pos[50010],L[50010],R[50010],klen,ksum,P,Q;
    pair<ll,ll>p[50010];
    deque<ll>q[50010];
    ll x(ll i)
    {
    	return p[i].first;
    }
    ll y(ll i)
    {
    	return p[i].second;
    }
    void build(ll l,ll r,ll id)
    {
    	q[id].clear();
    	ll sx=0,sy=0;
    	for(ll i=l;i<=r;i++)
    	{
    		sx+=(a[i]>=0)*a[i];
    		sy+=(a[i]<0)*a[i];
    		p[i]=make_pair(sx,sy);
    		while(q[id].size()>=2&&(y(q[id][q[id].size()-2])-y(q[id].back()))*(x(q[id].back())-x(i))<=(y(q[id].back())-y(i))*(x(q[id][q[id].size()-2])-x(q[id].back())))
    		{
    			q[id].pop_back();
    		}
    		q[id].push_back(i);
    	}
    }
    bool check(ll mid,ll id)
    {
    	return (y(q[id][mid])-y(q[id][mid+1]))*P<Q*(x(q[id][mid])-x(q[id][mid+1]));
    }
    ll divide_search(ll id)
    {
    	ll l=0,r=q[id].size()-1,mid,ans=0;
    	while(l<r)
    	{
    		mid=(l+r)/2;
    		if(check(mid,id)==true)
    		{
    			ans=mid+1;
    			l=mid+1;
    		}
    		else
    		{
    			r=mid;//mid 这个点可以被取到
    		}
    	}
    	return q[id][ans];
    }
    void init(ll n)
    {
    	klen=sqrt(n*log2(n));
    	ksum=n/klen;
    	for(ll i=1;i<=ksum;i++)
    	{
    		L[i]=R[i-1]+1;
    		R[i]=R[i-1]+klen;
    	}
    	if(R[ksum]<n)
    	{
    		ksum++;
    		L[ksum]=R[ksum-1]+1;
    		R[ksum]=n;
    	}
    	for(ll i=1;i<=ksum;i++)
    	{
    		for(ll j=L[i];j<=R[i];j++)
    		{
    			pos[j]=i;
    		}
    		build(L[i],R[i],i);
    	}
    }
    void update(ll x,ll y)
    {
    	P-=(a[x]>=0)*a[x];
    	Q-=(a[x]<0)*a[x];
    	a[x]=y;
    	P+=(a[x]>=0)*a[x];
    	Q+=(a[x]<0)*a[x];
    	build(L[pos[x]],R[pos[x]],pos[x]);
    }
    ll query()
    {
    	ll id=0,sum=0,sx=0,sy=0;
    	double ans=-0x7f7f7f7f;
    	for(ll i=1;i<=ksum;i++)
    	{
    		sum=divide_search(i);
    		if(1.0*(p[sum].first+sx)/P-1.0*(p[sum].second+sy)/Q-ans>eps)
    		{
    			ans=1.0*(p[sum].first+sx)/P-1.0*(p[sum].second+sy)/Q;
    			id=sum;
    		}
    		sx+=p[R[i]].first;//进行平移
    		sy+=p[R[i]].second;
    	}
    	return id;
    }
    int main()
    {
    	ll n,m,x,y,i;
    	cin>>n>>m;
    	for(i=1;i<=n;i++)
    	{
    		cin>>a[i];
    		P+=(a[i]>=0)*a[i];
    		Q+=(a[i]<0)*a[i];
    	}
    	init(n);
    	cout<<query()<<endl;
    	for(i=1;i<=m;i++)
    	{
    		cin>>x>>y;
    		update(x,y);
    		cout<<query()<<endl;
    	}
    	return 0;
    }
    

总结

  • \(T1\) 赛时一直在想小区间怎么合并到大区间,猜结论时不会合理分析样例和性质导致结论猜假了。 @KafuuChinocpp 讲的 Special Judge 写法不是很符合预期,本以为会对 2024初三集训模拟测试3 T4 计蒜客 T3729 MEX 有所启发。
  • \(T2\) 赛时和赛后转化题面转化得有点绕,很玄乎,没写完。
  • \(T3\) 的暴力写假了。

后记

  • 特殊奖励

    以下图片来源

  • \(T2\) 因为昨天 \(huge\) 看完题后,体活来找学长时说了需要 \(CDQ\) 分治所以就临时换了这道题,正常的话这道题应该和 暑假集训CSP提高模拟6 是一套题,还能和 \(T3\) 题面背景连上。

  • \(T4\) 夹带私货,剧透了《明末千里行》的部分情节。

posted @ 2024-07-25 15:32  hzoi_Shadow  阅读(117)  评论(0编辑  收藏  举报
扩大
缩小