• [做题笔记] 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\)

    • 如果 \(a_i\leq t \and t\leq b_i\),那么令 \(s_i=s_{i-1},s_{i+n}=s_{i+n-1}\)
    • 如果 \(t<a_i\),那么只增大 \(s_{i+n}\),令 \(s_i=s_{i-1},s_{i+n}=s_{i-1}+a_i\)
    • 如果 \(b_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);
    }
    
  • 相关阅读:
    object-c之kvc kvo(人中有属性数组、Book对象,数组中装载Book对象)
    object-c之通知
    大文件复制时进行实时保存
    通讯录
    object-c中对文件操作
    Foundation 框架之——NSDate
    Foundation 框架之——NSDictionary、NSMutableDictionary
    Objective-C之数据类型和表达式
    C语言-函数
    Storyboard类介绍
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/16387741.html
Copyright © 2020-2023  润新知