题解 luogu.P9489 ZHY 的表示法
题目
比较难想的一道组合数学题。
题意建模
题目理解
给定 \(n\) 个数 \(x_1, x_2, \dots, x_n\),问区间 \([l, r]\) 中有多少个正整数 \(x\) 满足:存在实数 \(y\),使得 \(\sum_{i=1}^n \lfloor \frac{y}{x_i} \rfloor = x\)。例如,\(n=2\),\(x_1=2, x_2=3\) 时,\(x=5\) 可取 \(y=6\) 满足条件。
算法分析
初步理解
在我拿到题目时,我并不知道如何入手。最关键的是,我连暴力都写不出来。本来想先混个 \(30-50\) 分的,忽然发现,\(x\) 是未知的,竟然 \(y\) 也是未知的。这个取整函数也不好处理。但是相对这两个位置量,我连暴力枚举都做不到。这是我的“卡点”。代表我思路的堵塞之处。带着这样的问题,我翻开了题解。
首先,需要知道的是,这两个变量怎么建立关系。其次,需要知道,怎么取理解这个下取整的意义(首先肯定要明白一点,就是本题肯定是有特殊性质的)。我们分别来分析。
-
两个变量之间的关系
显然每个 \(y\) 都唯一对应一个 \(x\),因此只需让每一个 \(x\) 都对应唯一一个 \(y\) 即可。我们可以发现\(x\) 实际上只与 \(y\) 除以 \(x\) 的商有关,和余数无关。这是一个很关键的性质。 -
单调性的发现
能想出这一步,基本是做出来了。反正,我没发现。当规定 \(x\) 的值是一定的时候,肯定会有一种现象:随着 \(y\) 的增加,\(x\) 的值也会增大。因此可以发现按此方法 \(x\) 关于 \(y\) 的函数是单调的,并且显然可以发现 \(y\) 关于 \(x\) 的函数也是单调的,因此 \(x\) 和 \(y\) 一一对应。
到现在,已经有了一个初步的想法:枚举 \(y\),看看能不能在 \(y\) 的变化中,存在 \(x\) 的整数变化。
- 计算。
可以发现,在 \(y\) 增大的过程中,若某一时刻 \(y\) 刚好是某个 \(x_{i}\) 的倍数,那么 \(y\) 除以 \(x_{i}\) 的商就会改变,\(x\) 就会改变,答案就应加 \(1\)。这也就是上文中说的变化观点。
整理思路如下:
关键思路
- 问题转换:对于给定的 \(x\),是否存在 \(y\) 使得 \(\sum \lfloor \frac{y}{x_i} \rfloor = x\)?直接枚举 \(y\) 不可行(范围太大)。
- 单调性观察:\(\sum \lfloor \frac{y}{x_i} \rfloor\) 随 \(y\) 增加非递减。当 \(y\) 增加时,和可能不变或增加(当 \(y\) 达到某些 \(x_i\) 的倍数时)。
- 容斥原理:统计 \(y\) 使得 \(\sum \lfloor \frac{y}{x_i} \rfloor\) 变化,即 \(y+1\) 是至少一个 \(x_i\) 的倍数。这样的 \(y\) 的个数等于 \(1\) 到 \(y_{\text{max}}\) 中能被至少一个 \(x_i\) 整除的数的个数(用容斥计算)。
算法步骤
- 二分查找:对于给定的上界 \(val\)(如 \(r\) 或 \(l-1\)),找到最大的 \(y\)(记为 \(res\))使得 \(\sum \lfloor \frac{y}{x_i} \rfloor \leq val\)。
- 容斥计算:计算 \(1\) 到 \(res\) 中能被至少一个 \(x_i\) 整除的数的个数(即 \(cal(res)\))。
- 答案求解:区间 \([l, r]\) 的答案即为 \(solve(r) - solve(l-1)\),其中 \(solve(val)\) 返回 \(cal(res)\)。
时间复杂度
- 二分查找:\(O(\log (2 \times 10^{18}) \times n) \approx 60n\)。
- 容斥计算:枚举所有 \(2^n\) 个子集,计算 LCM。最坏 \(O(2^n \times n)\),但实际因 LCM 增长快,常可提前终止。
- 总复杂度:\(O(\log y_{max}) \times n + 2^n \times n)\),\(n=25\) 时可行。
参考代码
#include<iostream>
#define int __int128
#define rei register int
const int N=30;
int a[N];
int n,l,r;
inline int read()
{
int f=1,x=0;
char c=getchar();
while(c<'0' || c>'9') { if(c=='-') f=-1; c=getchar(); }
while(c>='0' && c<='9') { x=(x<<1)+(x<<3)+(c^48); c=getchar(); }
return f*x;
}
void write(int x)
{
if(x<0) putchar('-'),x=-x;
if(x>9) write(x/10);
putchar(x%10+'0');
}
int get1(int n)
{
int count=0;
while(n)
{
n&=(n-1); // 消除最低位的1
count++;
}
return count;
}
int gcd(int a,int b) { return b?gcd(b,a%b):a; }
int lcm(int a,int b) { return a/gcd(a,b)*b; }
bool check(int x,int y)
{
int sum=0;
for(rei i=1;i<=n;++i) sum+=y/a[i];
return sum<=x;
}
int cal(int x)
{
int all=(1<<n)-1,ans=0;
for(rei i=1;i<=all;++i)
{
int cnt=get1(i),sum=1;
for(rei j=1;j<=n;++j)
{
if(!(i&1<<j-1)) continue;
sum=lcm(sum,a[j]);
if(sum>x) break;
}
if(cnt&1) ans+=x/sum;
else ans-=x/sum;
}
return ans;
}
int solve(int val)
{
int L=-1,R=2e18,res;
while(L<=R)
{
int mid=L+R>>1;
if(check(val,mid)) res=mid,L=mid+1;
else R=mid-1;
}
return cal(res);
}
signed main()
{
n=read(),l=read(),r=read();
for(rei i=1;i<=n;++i) a[i]=read();
write(solve(r)-solve(l-1)); putchar('\n');
return 0;
}
细节实现
代码注释
int solve(int val) {
int L = -1, R = 2e18, res;
while (L <= R) {
int mid = (L + R) >> 1;
if (check(val, mid)) { // 检查 Σ floor(mid/x_i) ≤ val
res = mid;
L = mid + 1;
} else R = mid - 1;
}
return cal(res); // 计算1到res中能被至少一个x_i整除的数的个数
}
int cal(int x) {
int ans = 0;
for (int i = 1; i < (1 << n); ++i) { // 枚举所有非空子集
int cnt = __builtin_popcount(i); // 子集大小
int sum = 1;
for (int j = 0; j < n; ++j) {
if (i & (1 << j)) {
sum = lcm(sum, a[j]); // 计算子集LCM
if (sum > x) break; // 提前终止
}
}
if (cnt % 2) ans += x / sum; // 奇加偶减
else ans -= x / sum;
}
return ans;
}
总结归纳
本题的切入点比较隐晦。属于是非常难以挖掘。切入点如下:
-
发现下取整函数只跟倍数有关。由此可以推得 \(y\) 不论是整数还是实数,都不影响答案的统计;
-
基于上面一点,用一种变化的视角理解,就是当 \(y\) 变化的时候,如果 \(x\) 也跟着变化,那么就是算进答案中,成为一点贡献;
-
又由上,发现是一个函数的模型,所以研究函数的基本性质,自然会联想到单调性(因为也是与 OI 竞赛联系最紧的),所以到这里,基本上思路就通了。
问题是,我还是不理解这种分析过程是怎样在大脑产生的。还是不得不感叹:容斥原理好难啊。。。

浙公网安备 33010602011771号