暑假集训 加赛1

暑假集训 加赛1

组题人: @Chen_jr

\(T1\) P145 修仙(restart)

  • 原题: 2022NOIP A层联测13 T4 修仙(restart)

  • 题目中的 可以恰好

  • 考虑计算双休带来的差值。

  • \(b_{i}=a_{i}+a_{i+1}-\max(a_{i}+a_{i+1},0)\) ,则需要计算从 \([1,n)\) 中选出 \(k\) 个不相邻的 \(b_{i}\) 的最大价值,同 luogu P3620 [APIO/CTSC2007] 数据备份 | CF958E2 Guard Duty (medium) | SP1553 BACKUP - Backup Files | luogu P1484 种树 | luogu P1792 [国家集训队] 种树

  • 然后就是反悔贪心板子了。

    • 基本思想是无论当前的选项是否最优都接受,然后进行比较,如果选择之后不是最优了,则反悔,舍弃掉这个选项,常写作两种决策之间作差的形式;否则,正式接受。如此往复。
    • 容易有在求解最大值/最小值的最优解中,最大值/最小值左右两端的数要么都选要么都不选。
    • \(k\) 的状态能由 \(k-1\) 的状态继承而来,启发我们可以依次处理子问题。
    • 算法流程:先选出 \(\{ b \}\) 中的最大值 \(b_{pos}\) 加入答案集合,然后删除 \(b_{pos-1},b_{pos+1}\) ,用 \(b_{pos-1}+b_{pos+1}-b_{pos}\) 替代原来的 \(b_{pos}\)
      • 若选了 \(b_{pos-1}+b_{pos+1}-b_{pos}\) 等价于将原来答案集合里的 \(b_{pos}\) 替代成 \(b_{pos-1}+b_{pos+1}\)
      • 否则选择 \(b_{pos}\)
    • 双向链表维护动态插入和删除;优先队列维护每次选出最大值,需要懒惰删除法。
    • 边界 \(0\)\(n+1\) 一般需要特殊处理。
      • 实际上是对 \(1,n\) 的处理。
    点击查看代码
    priority_queue<pair<ll,ll> >q;
    ll a[200010],b[200010],cha[200010],l[200010],r[200010],vis[200010];
    int main()
    {
        ll n,x,k,ans=0,pos,i;
        cin>>n>>x;
        k=n/2;
        for(i=1;i<=n;i++)
        {
            cin>>a[i];
            ans+=a[i];
        }
        for(i=1;i<=n-1;i++)
        {
            b[i]=a[i]+a[i+1]-max(a[i]+a[i+1]-x,0ll);
            l[i]=i-1;
            r[i]=i+1;
            q.push(make_pair(b[i],i));
        }   
        b[0]=b[n]=-0x3f3f3f3f3f3f3f3f;//保证不会被选到
        for(i=1;i<=k;i++)
        {
            while(q.empty()==0&&vis[q.top().second]==1)
            {
                q.pop();
            }
            if(q.empty()==0)
            {
                ans-=q.top().first;
                pos=q.top().second;
                q.pop();
                vis[l[pos]]=vis[r[pos]]=1;
                b[pos]=b[l[pos]]+b[r[pos]]-b[pos];
                q.push(make_pair(b[pos],pos));
                l[pos]=l[l[pos]];
                r[pos]=r[r[pos]];
                l[r[pos]]=r[l[pos]]=pos;
            }
            cout<<ans<<endl;
        }
        return 0;
    }
    

\(T2\) T3682. 七负我

  • 不难发现,完全子图对答案的贡献最大。
  • 设最后得到的最大子图/团大小为 \(siz\) ,则 \((\frac{x}{siz})^{2} \times \frac{siz(siz-1)}{2}\) 即为所求。
  • 现在问题来到了怎么求最大子图/团,常采用 Bron–Kerbosch 算法或折半搜索。
    • Bron–Kerbosch 算法
      • 维护 \(R/P/X\) 三个集合,分别表示当前正在找的极大团里的点/有可能加入当前正在找的极大团里的点/已经找到的极大团内的点。

        • \(X\) 用于判断找到的团先前有没有找过,在统计最大团方案数时需要用到。
      • 算法流程如下

        • \((1)\) 初始化 \(R,X\) 为空集, \(P\) 为包含所有节点的集合。
        • \((2)\)\(P\) 中一点 \(u\) ,设 \(Q_{u}\) 表示所有与 \(u\) 有边相邻的点的集合,递归集合 \(R \bigcup u,P \bigcap Q_{u},x \bigcap Q_{u}\) 。递归过程中若出现 \(P,X\) 均为空的情况,则 \(P\) 内部的点就构成了一个极大子团,统计答案。
          • 要保证加入 \(u\) 后仍是团,必须保证加入后 \(P'\) 中所有点都与 \(u\) 相连,故取并集。
            • 这实际上是设定关键点优化的一部分。
        • \((3)\)\(u\)\(P\) 中删除,并加入到 \(X\) 中。
        • \((4)\) 重复 \((2),(3)\) 操作直至 \(P\) 为空。
      • 优化

        • 及时剪枝
        • 在开始时把所有点排序,枚举时按照下标顺序,防止重复
        • 设定关键点
          • 若将 \(u\) 加入 \(R\) 后,再取与 \(u\) 有边相连的点 \(v\) 加入 \(R\) 后仍是一个极大团,那么加入 \(u,v\) 的顺序并不影响答案。
          • 所以将 \(u\) 加入 \(R\) 后,再取出与 \(u\) 没有边相连的点进行递归。
        点击查看代码
        int dis[50][50],R[50][50],P[50][50],X[50][50],siz=0;
        void Bron_Kerbosch(int dep,int r,int p,int x)//dep 表示当前搜到第几层
        {
        	if(p==0&&x==0)//若 x 不等于 0 说明这个团之前已经算过了,才能用来统计方案数
        	{
        		siz=max(siz,r);
        	}
        	else
        	{
        		int u=P[dep][1];
        		for(int i=1;i<=p;i++)
        		{
        			if(dis[u][P[dep][i]]==0)//因为 dis[P[dep][1]][P[dep][1]]=0 所以一开始会算一次
        			{
        				for(int j=1;j<=r;j++)//继承
        				{
        					R[dep+1][j]=R[dep][j];
        				}
        				R[dep+1][r+1]=P[dep][i];//加入
        				int np=0,nx=0;
        				for(int j=1;j<=p;j++)
        				{
        					if(dis[P[dep][i]][P[dep][j]]!=0)
        					{
        						np++;
        						P[dep+1][np]=P[dep][j];//取并集
        					}
        				}
        				for(int j=1;j<=x;j++)
        				{
        					if(dis[P[dep][i]][X[dep][j]]!=0)
        					{
        						nx++;
        						X[dep+1][nx]=X[dep][j];//取并集
        					}
        				}
        				Bron_Kerbosch(dep+1,r+1,np,nx);
        				x++;
        				X[dep][x]=P[dep][i];//加入
        				P[dep][i]=0;//删除
        			}
        		}
        	}
        }
        int main()
        {
        	int n,m,x,u,v,i;
        	cin>>n>>m>>x;
        	for(i=1;i<=m;i++)
        	{
        		cin>>u>>v;
        		dis[u][v]=dis[v][u]=1;
        	}
        	for(i=1;i<=n;i++)
        	{
        		P[1][i]=i;
        	}
        	Bron_Kerbosch(1,0,n,0);
        	printf("%.6lf\n",(1.0*x/siz)*(1.0*x/siz)*siz*(siz-1)/2);
        	return 0;
        }
        
    • 折半搜索
      • 以下部分贺的官方题解,改了下 \(\LaTeX\)

        我们将所有点分为两个集合 \(A,B\) ,对于 \(A\) 中的所有点,维护 \(f_{S}\) 表示集合 \(S\) 中所有点组成子图中最大团大小。

        之后枚举 \(B\) 中的最大团 \(T\) ,找到最大团中所有节点向 \(S\) 集合连边的交集,显然 \(\operatorname{popcount}(T)+f_S\) 可以更新答案。

总结

  • 科普场。
  • 反悔贪心是去年听 \(Accoder\) 的多校联训的时候写的,只学了写法,没学怎么将题面转化成反悔贪心可解决的问题,导致 \(T1\) 没看出来是反悔贪心。

后记

  • 成分复杂。

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