[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);
}