[做题笔记] pb大师的杂题选讲

[ARC117F] Gateau

题目描述

点此看题

有一个长度为 \(2n\) 的环形蛋糕,现在要往上面放草莓。

对于每个 \(i\),都有限制 \(i,i+1...i+n-1\) 位置上的草莓总数至少是 \(a_i\)(注意蛋糕是环形的)

问至少要放几个草莓。

\(n\leq 1.5\cdot 10^5\)

解法

很容易想到对于前缀和建立差分约束,但由于是环我们要分类讨论:

  • 如果 \(i<n\)\(s_{i+n}-s_i\geq a_i\)
  • 如果 \(i\geq n\)\(s_{2n}-s_{i}+s_{i-n}\geq a_i\)

可以二分 \(s_{2n}\),那么第二类限制就可以写成 \(s_{2n}-a_i\geq s_{i}-s_{i-n}\),这样我们可以把现在全部化归到左半边。那么合法 \(s\) 数组的要求是:\(s_{i+n}-s_i\in[l_i,r_i]\),并且 \(s\) 不降。

不能直接跑差分约束,考虑到所有限制区间长度为 \(n\) 的这个条件,我们考虑确定 \(s_n\) 的取值。如果已知 \(s_n\) 的取值会有这样一种贪心算法,我们按顺序扫描 \(i=1,2...n-1\),设 \(t=s_{i+n-1}-s_{i-1}\)

  • 如果 \(l_i\leq t \and t\leq r_i\),那么令 \(s_i=s_{i-1},s_{i+n}=s_{i+n-1}\)
  • 如果 \(t<l_i\),那么只增大 \(s_{i+n}\),令 \(s_i=s_{i-1},s_{i+n}=s_{i-1}+a_i\)
  • 如果 \(r_i<t\),那么只增大 \(s_i\),令 \(s_i=s_{i+n-1}-b_i,s_{i+n}=s_{i+n-1}\)

不难发现上面每一步都是选择了最少的增量,所以该贪心是正确的。贪心之后我们只需要检查 \(s_{n-1}\leq s_n\and s_{2n-1}\leq s_{2n}\) 是否成立即可,如果成立我们就找到了合法解。

考虑如果 \(s_n\) 增大,那么 \(s_{2n-1}\) 只会增大,并且 \(s_n\) 越大对于 \(s_{n-1}\leq s_n\) 条件的判定是越优的。所以我们再通过一次二分找到最大满足 \(s_{2n-1}\leq s_{2n}\)\(s_n\),然后检验它是否满足 \(s_{n-1}\leq s_n\) 即可。

所以最终的实现就是两层二分,时间复杂度 \(O(n\log ^2 n)\)

总结

本题的的关键条件是每个限制长度都为 \(n\),而一个限制要么包含 \(n\),要么包含 \(2n\),所以可以把这两个关键点的取值弄出来就很方便做。这说明不等式规划问题中,确定关键点的取值是重要的

#include <cstdio>
#include <cassert>
#include <iostream>
using namespace std;
const int M = 300005;
#define int long long
#define pii pair<int,int>
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M],b[M],s[M];
pii calc(int x)
{
	int l=0,r=x;
	for(int i=1;i<n;i++)
	{
		int t=r-l;
		if(t<a[i]) r=l+a[i];
		if(b[i]<t) l=r-b[i];
	}
	return {l,r};
}
int check(int x)
{
	for(int i=0;i<n;i++)
	{
		b[i]=x-a[i+n];
		if(b[i]<a[i]) return 0;
	}
	int l=a[0],r=b[0],p=l;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(calc(mid).second<=x)
			p=mid,l=mid+1;
		else r=mid-1;
	}
	pii t=calc(p);
	return t.first<=p && t.second<=x;
}
signed main()
{
	n=read();m=n<<1;
	for(int i=0;i<m;i++) a[i]=read();
	int l=0,r=1e9,ans=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid)) ans=mid,r=mid-1;
		else l=mid+1;
	}
	printf("%lld\n",ans);
}

新年的腮雷

题目描述

点此看题

解法

众所周知,合并问题有其树形结构,我们可以把问题转化成:场上有若干个有根树,我们从中选取 \(m\) 个有根树,按照题目的方式把他们合并成一棵树,合并到无法合并为止,最小化最后树根的点权。

但是这样还是不好贪心,我们考虑逆向这个过程,即二分最后树根的点权,然后把树上的叶子拆分。最后我们只需要判定是否 \(a\) 中每个元素都能匹配上比它大的元素。

形式化地说,我们有两个集合 \(S,T\),要求把集合 \(S\) 拆分成集合 \(T\),如果 \(|S|>|T|\) 则无解,如果 \(|S|=|T|\) 则用上述方式进行判定。如果 \(|S|<|T|\),我们考虑如下贪心规则进行拆分:

  • 如果 \(\max S<\max T\),那么一定匹配不上,可以直接判定无解。
  • 如果 \(\max T\leq \max S<\max T+b_1\),这说明拆了 \(\max S\) 之后就寄了,可以直接寻找 \(S\) 中第一个 \(>\max T\) 的元素 \(x\),然后把 \(\max T\)\(x\) 匹配即可。
  • 如果 \(\max T+b_1\leq \max S\),那么拆分 \(\max S\) 一定是最优的,并且不会导致无法匹配的情况。

multiset 模拟这个过程,时间复杂度 \(O(n\log n\log A)\)

总结

使用贪心法时,可以尝试减少贪心主体的数量。比如原来我们要取 \(m\) 个树合并,并不好规划;但是逆向操作之后只需要把一个叶子拆成 \(m\) 个点,就可以贪心了。

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
const int M = 50005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M],b[M];
int check(int x)
{
	multiset<int> s;
	s.insert(x);int p=n;
	while(!s.empty() && s.size()<p)
	{
		int t=*s.rbegin();
		if(t>=a[p]+b[1])
		{
			s.erase(--s.end());
			for(int i=1;i<=m;i++)
				s.insert(t-b[i]);
		}
		else if(t>=a[p])
			s.erase(s.lower_bound(a[p--]));
		else return 0;
	}
	if(s.size()==p)
	{
		int i=1;
		for(auto x:s)
		{
			if(x<a[i]) return 0;
			i++;
		}
		return 1;
	}
	return 0;
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<=m;i++) b[i]=read();
	sort(a+1,a+1+n);
	sort(b+1,b+1+m);
	int l=a[n],r=1e12,ans=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid)) ans=mid,r=mid-1;
		else l=mid+1;
	}
	printf("%lld\n",ans);
}

[AGC040F] Two Pieces

题目描述

点此看题

解法

注意到题目的方案数是根据两个棋子每个时刻的位置定义的,所以直接规划操作序列就会算重。我们可以把操作序列上加一点限制,让操作序列数和方案数完全等效起来。

考虑现在较大点的坐标是 \(x\),较小点和它的距离是 \(d\),记为状态 \((x,d)\),那么操作写成:

  • 移动较大的棋子:\(x,d\) 同时增加 \(1\)
  • 移动较小的棋子:\(d\geq2\) 的条件下,让 \(d\) 减少 \(1\)
  • 使用瞬移操作:让 \(d\) 直接变为 \(0\)

现在可以直接对操作序列计数了,我们先考虑只有前两种操作的情况。枚举较小点的坐标 \(k\),较大点的坐标一定是 \(B\),总的消耗次数就是 \(k+B\),由于限制可以表示成 \(d>0\) 始终成立,相当于网格图上不碰到 \(y=x\) 这条直线,所以只考虑前两种操作的操作序列数可以直接用卡特兰数计算。

考虑把操作三插入到原来的操作序列中,假设我们要把插入第 \(i\) 操作后面,它对应的距离是 \(d_i\),那么可以插入的充要条件是:不存在 \(i<j\) 的点 \(j\),满足 \(d_i\geq d_j\);因为如果存在就会和 \(d>0\) 始终成立的限制相违背。

一个关键的 \(\tt observation\) 是:只有最靠后的三操作是有实际影响的。换句话说,就是只要确定了最靠后的三操作,其他的三操作怎么插入,插入到哪里我们都是不关心的(但要保证插入合法)

最靠后的三操作一定在最后一次 \(d_i=A-k\) 的位置 \(i\) 后。根据介值定理,其他的三操作插入且仅能插入在最后一次 \(d_j=0,1,2...A-k\) 的位置 \(j\) 后面,所以我们把剩下的三操作任意分配到这些位置,用个隔板法计算方案数。

时间复杂度 \(O(n)\)

总结

可以通过添加限制,把方案数转化为易于统计的形式。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 10000005;
const int MOD = 998244353;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,a,b,ans,fac[M],inv[M];
void add(int &x,int y) {x=(x+y)%MOD;}
void init()
{
	fac[0]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=2;i<=n;i++) inv[i]=inv[i-1]*inv[i]%MOD;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%MOD;
}
int C(int n,int m)
{
	if(n<m || m<0) return 0;
	return fac[n]*inv[m]%MOD*inv[n-m]%MOD;
}
int walk(int k)
{
	return (C(k+b-1,b-1)-C(k+b-1,k-1)+MOD)%MOD;
}
signed main()
{
	n=read();a=read();b=read();init();
	if(a==0 && b==0) {puts("1");return 0;}
	for(int k=0,m=min(n-b,min(a,b-1));k<=m;k++)
	{
		int c=k?walk(k):1;
		if(n==b+k)
		{
			if(k==a) add(ans,c);
		}
		else
		{
			int x=n-b-k-1,y=a-k+1;
			add(ans,c*C(x+y-1,y-1));
		}
	}
	printf("%lld\n",ans);
}

[AGC026F] Manju Game

题目描述

点此看题

\(n\) 个盒子摆成一排,第 \(i\) 个盒子的权值是 \(a_i\),两人轮流操作,每次操作的方法如下:

  • 设上一个人选择的盒子是 \(i\),如果 \(i\) 存在,并且 \(i−1,i+1\) 中至少有一个还没被选择过的盒子,那么就在 \(i − 1, i + 1\) 中选一个未被选过的盒子,取走其中的权值。
  • 否则,可以任选一个未被选过的盒子,取走其中的权值。
  • 如果每个盒子都被选过了则结束游戏。

两人都希望最大化自己拿到的权值,求两人最终分别能拿到多少权值。

\(n\leq 3\cdot 10^5\)

解法

pb指导:博弈题可以先想一个傻逼一点的策略,然后再修正他。

听从上述建议,我们可以从一个简单策略入手。最简单的策略就是先手选择边界上的权值,当先手做出决定的时候,游戏就已经结束了。但是这种策略其实也有可取之处,就是后手无法做出选择,主动权全在先手手上

这启发我们进行关于主动权的讨论,而讨论主动权势必涉及到 \(n\) 的奇偶性,所以按照 \(n\) 的奇偶性分类讨论。

如果 \(n\) 是偶数,那么如果先手选择非边界,会把剩下的点分成奇数段和偶数段。那么如果后手选择在偶数段操作,在和获得简单策略不优权值的情况下,先手会失去主动权,所以先手不会再非边界操作,这说明我们可以直接应用简单策略。

如果 \(n\) 是奇数,那么如果先手选择奇数点,会把剩下的点分成两个偶数段,类比上面的讨论先手不会这样做。所以先手要么应用简单策略,要么选取一个偶数点操作。

考虑先手选取偶数点的情况,后手会消去一个区间,然后问题会向另一个递归,此时主动权仍然在先手手上。注意这个过程构成了一个树形结构。那么问题可以转化成,先手先钦定一个二叉树,上面的节点都是偶数节点,后手可以自由选择走到哪个叶子。

所以我们要最大化“走到所有叶子对应的最小赚取量”,其中赚取量定义为:从根走到这个叶子剩下的区间中,奇数位置减去偶数位置的权值(因为叶子就是问题的出口了,所以这个区间应该直接应用简单策略,赚取量就是和直接取偶数位置的差值)

考虑二分最大赚取量 \(x\),我们再把问题放在序列上来。问题变成了求是否存在一个偶数点的划分方案,使得相邻两个偶数点之间的 ”奇数位置减去偶数位置的权值“ 都大于 \(x\)(特别地,序列的左右边界视为有两个偶数点)

考虑贪心,从左往右扫描,维护一个最优划分点 \(j\);如果当前点 \(i\)\(j\) 划分后它们之间满足条件,那么看看 \(s[i]\) 是否比 \(s[j]\) 小(\(s\) 是奇数位置减去偶数位置权值的前缀和),如果是的话,把 \(i\) 作为最优划分点。

时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 300005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,s,a[M],b[M];
int check(int x)
{
	int mi=0;
	for(int i=1;i<n;i+=2)
		if(b[i]-mi>=x) mi=min(mi,b[i+1]);
	return b[n]-mi>=x;
}
signed main()
{
	n=read();
	for(int i=1;i<=n;i++) s+=a[i]=read();
	if(n%2==0)
	{
		int s1=0,s2=0;
		for(int i=1;i<=n;i++)
		{
			if(i&1) s1+=a[i];
			else s2+=a[i];
		}
		printf("%d %d\n",max(s1,s2),s-max(s1,s2));
		return 0;
	}
	for(int i=1;i<=n;i++)
	{
		if(i&1) b[i]=b[i-1]+a[i];
		else b[i]=b[i-1]-a[i];
	}
	int l=0,r=n*1000,ans=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(check(mid)) ans=mid,l=mid+1;
		else r=mid-1;
	}
	for(int i=1;i<=n;i++)
		if(i%2==0) ans+=a[i];
	printf("%d %d\n",ans,s-ans);
}
posted @ 2022-06-18 09:47  C202044zxy  阅读(392)  评论(0编辑  收藏  举报