「AGC036F」Square Constraints 题解
本文网址:https://www.cnblogs.com/zsc985246/p/16621307.html ,转载请注明出处。
2023/5/11update:修 。
传送门
题目大意
给定一个整数 ,求有多少种 到 的排列 ,使得对于每个 ,都有 。输出答案对给定的 取余的结果。
思路
通过 这个式子,我们很容易想到用 的方案数减去 的方案数。但是我们发现实际上不可做。不过这种想法提示我们可以考虑容斥。
首先,我们提炼一下问题模型:
- 对于一个 组成的序列 ,限制 ,求方案数。
我们考虑这个问题的弱化版:
- 对于一个 组成的序列 ,限制 ,求方案数。
我们考虑将 数组从小到大排序,那么这个弱化版的答案为:
证明:
首先将 数组从小到大排序。
对于这个排列 ,我们肯定先从 小的开始放。所以第一步的方案数为 。
再考虑下一个数 。此时剩下 个数。因为 从小到大,所以上一轮选走的数一定小于等于 。此时的方案数为 。
因为这些步骤是递进的,所以方案数是每一步的方案数的乘积。最终方案数即为:
证毕。
而原问题模型多了一个限制条件,不难想到将满足两个条件的最大值都设出来,综合在一起,套用公式得到答案。
回到本题,我们将满足条件的最大值设出来。
我们尝试设 为满足 的 的最大值, 为满足 的 的最大值。其中 。
通过观察我们不难发现:
-
和 两个函数值随着 的增大而减小。
和 为定值, 增大,则 增大,那么 就会减小。
-
满足 。
当 时,,要使 ,则 ,无解。
-
。
由于第一条规律: 即 , 即 。
有了这些东西,我们就可以将限制拆分为:
所有的数都满足 ,且至少有 个数满足 ,也就是说在前面 个位置选出 个 。
然后就可以进行容斥了。
什么?你不会容斥?你可以翻到最下面的补充知识。
在弱化版中,我们将 数组从小到大进行了排序。
所以我们类比一下,考虑将 和 两个函数打包成二元组排序。由于第三条规律,我们通过如下方式打包成二元组:
接下来按第一关键字进行排序。
接下来就是求解方案数。我们可以使用 求解。
设 表示在前 个位置上选出了 个 的方案数。
在状态转移时需要用到当前数在选出的位置上的排名,所以用两个变量 和 ,分别统计前面 和 的个数。
首先枚举 ,从 到 ,表示在前面 个位置选出 个 ;
然后枚举 ,从 到 ,表示已经确定了前面 个位置;
最后枚举 ,从 到 ,表示前面已经选出了 个 ;
状态转移分情况讨论:
-
当前二元组是 :
-
选择 :
- 排名:
前面选出的 个 一定比 小;
又因为排了序,前面的 个 肯定也比 小。
所以总共有 个数比 小。
- 转移:
-
-
当前二元组是 :
-
选择 :
- 排名:
由于函数单调递减,所以 个二元组 相对 一定排在前面;
我们选出的所有 个 都比 小;
之前 类型的二元组选出的 个 也都比 小。
所以总共就有 个数比 小。
- 转移:
-
选择 :(注意只能选 个)
- 排名:
前面选出的 个 一定比 小;
前面选出的 个 也比 小;
总共就有 个数比 小。
- 转移:
-
最后综合起来求出 ,容斥即可。
代码实现
#include<bits/stdc++.h>
#define ll long long
const ll N=501;
using namespace std;
ll n,p;
ll f[N][N];
vector<pair<ll,ll>>t;//记录二元组
ll F(ll i){//F函数
ll ans=2*n-1;
while(ans>=0&&i*i+ans*ans>4*n*n)ans--;//注意是大于
return ans+1;
}
ll G(ll i){//G函数
ll ans=2*n-1;
while(ans>=0&&i*i+ans*ans>=n*n)ans--;//注意是大于等于
return ans+1;
}
int main(){
scanf("%lld%lld",&n,&p);
for(ll i=0;i<2*n;i++){//预处理二元组
if(i<n)t.push_back({G(i),F(i)});
else t.push_back({F(i),0});
}
sort(t.begin(),t.end());//对二元组按第一关键字排序
ll ans=0;
for(ll k=0;k<=n;k++){
//初始化
memset(f,0,sizeof(f));
f[0][0]=1;
ll t1=0,t2=0;//辅助统计变量
for(ll i=0;i<t.size();i++){
for(ll j=0;j<=k;j++){
if(!t[i].second){//(F(i),0)
f[i+1][j]=(f[i+1][j]+f[i][j]*(t[i].first-j-t1)%p)%p;//选F(i)
}else{//(G(i),F(i))
if(j<k){//选了之后不能超过k个
f[i+1][j+1]=(f[i+1][j+1]+f[i][j]*(t[i].first-j-t1)%p)%p;//选G(i)
}
f[i+1][j]=(f[i+1][j]+f[i][j]*(t[i].second-n-k-t2+j)%p)%p;//选F(i)
}
}
if(!t[i].second)t1++;
else t2++;
}
//容斥
if(k%2)ans=(ans-f[t.size()][k]+p)%p;
else ans=(ans+f[t.size()][k])%p;
}
printf("%lld",ans);
return 0;
}
补充知识
容斥原理
容斥原理是一种组合数学常用的方法。
比如说,你要计算几个集合并集的大小,我们可以先将所有单个集合的大小加起来,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分……依此类推,最终你就可以得出答案。
用数学公式可以表示为:
广义容斥原理
容斥原理求的是不满足任何性质的方案数,我们通过计算所有至少满足 个性质的方案数之和来计算。
同样的,我们可以通过计算所有至少满足 个性质的方案数之和来计算恰好满足 个性质的方案数。这样的容斥方法我们称之为广义容斥原理。
一般我们会用二项式反演进行计算。
二项式反演
对于函数 和 ,满足:
则:
它一般用于"恰好"和"至少""至多"的转换式中。
举个例子:
「bzoj2839」集合计数
一个有 个元素的集合有 个不同子集(包含空集),现在要在这 个集合中取出至少一个集合,使得它们的交集的元素个数为 ,求取法的方案数模 。
。
由题列出式子:。即钦定 个交集元素,则包含这 个的集合有 个;每个集合可选可不选,但不能都不选。
设 表示钦定交集元素为至少 个的方案数, 表示钦定交集元素恰好为 个的方案数,则有 。
这个时候我们就可以利用二项式反演,求得 。
这样就可以 求出答案。
尾声
如果你发现了问题,你可以直接回复这篇题解
如果你有更好的想法,也可以直接回复!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通