Luogu U657513 数点

题目链接

今年 2 月为某场比赛供的题。
当时遗留了一些问题,后来问 ChatGPT 得到了解决,因此重新发一篇题解。

Description

已知 \(f(x,y)=\begin{cases}x+y,&x=0~{\rm or}~y=0,\\f(\lfloor x/2\rfloor ,y)+f(x,\lfloor y/2\rfloor ),&x>0,y>0.\end{cases}\)

求满足 \(f(x,y)\le n\) 的整数对 \((x,y)\) 的个数。

Solution: Part 1

首先确定一下 \(f(x,y)\) 的量级。
可以证明(或打表得出):当 \(x>0,y>0\) 时,\(\dfrac12 xy\le f(x,y)\le 2xy.\)

\(f(x,y)\le n\),则 \(xy\le 2f(x,y)\le 2n\)

这意味着只需对 \(O(\sqrt n)\) 行作计数。具体来说:

  • 将满足条件的 \((x,y)\) 分类:\((1)x<y; (2)x=y; (3)x>y\)
  • 显然 \((2)\) 容易计数,而 \((1)\)\((3)\) 是对称的,所以只需考虑 \((1)\)
  • 此时 \(x^2<xy\le 2n\),故 \(x\le\sqrt{2n}\)。也就是只有 \(O(\sqrt n)\) 行需要计数。

现在对于每个 \(x\),需要求出最大的满足条件的 \(y\)(记为 \(Y(x)\))。
显然 \(f(x,y)\) 关于 \(y\) 单调增,因此可以使用二分查找。

设计算 \(f(x,y)\) 的复杂度为 \(T(n)\),则算法的总复杂度为 \(O(\sqrt n\log n\cdot T(n))\)

Solution: Part 2

不妨直接递归计算 \(f(x,y)\)
递归中只会出现 \(f(\lfloor x/2^i\rfloor,\lfloor y/2^j\rfloor)\) 这些点,共 \(O(\log^2n)\) 个,
那么用 DP 或记忆化搜索实现,就可以做到 \(T(n)=O(\log^2n)\),总复杂度为 \(O(\sqrt n\log^3n)\)

还可以将 \(f(x,y)\) 的递归式完全展开。
\(A=\lfloor\log_2x\rfloor,B=\lfloor\log_2y\rfloor\),有:

\[f(x,y)=\sum_{i=0}^Af(\lfloor x/2^i\rfloor,0)\binom{i+B}{i}+\sum_{j=0}^Bf(0,\lfloor y/2^j\rfloor)\binom{j+A}{j}. \]

预处理组合数后利用此式计算,则 \(T(n)=O(\log n)\),总复杂度为 \(O(\sqrt n\log^2n)\)

Solution: Part 3

前面已经说明 \(f(x,y)=\Theta(xy)\),因此 \(Y(x)=\Theta\left(\dfrac nx\right)\),即 \(\log_2 Y(x) = \log_2\dfrac nx+O(1)\)
那么通过 \(O(1)\) 次查询 \(f\) 值,可以确定 \(\lfloor \log_2 Y(x)\rfloor\)

再次观察 \(f(x,y)\) 的表达式:

\[f(x,y)=\sum_{i=0}^A\lfloor x/2^i\rfloor\binom{i+B}{i}+\sum_{j=0}^B\lfloor y/2^j\rfloor\binom{j+A}{j}. \]

考察 \(y\) 每个二进制位的贡献。记 \(\displaystyle y=\sum_k 2^k\),则

\[\begin{aligned} \sum_{j=0}^B \lfloor y/2^j\rfloor \binom{j+A}{j} & = \sum_{j=0}^B \sum_{k\ge j} 2^k/2^j \binom{j+A}{j} \\ & = \sum_k \underbrace{\sum_{j=0}^k 2^{k-j} \binom{j+A}{j}}_{w_k}. \end{aligned}\]

可得递推式 \(\displaystyle w_0=1,w_k=2w_{k-1}+\binom{k+A}{k}(k>0)\)。由此能预处理 \(w_0,\cdots,w_B\)

考虑一种类似倍增的做法:
从高到低枚举 \(y\) 的每个二进制位,检查当前位能否设为 \(1\)
若将第 \(k\) 位设为 \(1\),则将当前 \(f\) 值加上 \(w_k\)。只需保证 \(f\) 值不超过 \(n\)

至此得到 \(O(\log n)\) 计算 \(Y(x)\) 的做法。那么总复杂度为 \(O(\sqrt n\log n)\)

Solution: Part 4?

打表发现 \(Y(x)\) 存在大量重复,不同取值的个数其实很少。

因此可以按 \(Y(x)\) 的值分块计算:

  • 对于当前 \(x\),求出 \(Y(x)\)
  • \(x,x+1,\cdots,Y(Y(x))\)\(Y\) 值均为 \(Y(x)\)。对整个块进行计数。
  • 跳转至下个块:\(x\leftarrow Y(Y(x))+1\)

我曾怀疑这一做法拥有更优的复杂度,然而事实并非如此。
ChatGPT 给出了一族反例以及构造思路:

\(\lfloor \log_2x\rfloor = \lfloor \log_2y\rfloor = m\)
\(L = 2^m\),则 \(L\le x,y \le 2L-1\)

回顾 \(f(x,y)\) 的表达式:

\[f(x,y)=\sum_{i=0}^m\left(\lfloor x/2^i\rfloor+\lfloor y/2^i\rfloor\right)\binom{i+m}{i}. \]

考虑 \(x+y=3L-1\) 的部分,也即 \((x,y)=(L,2L-1),(L+1,2L-2),\cdots,(2L-1,L)\)

注意到 \(\lfloor x/2^i\rfloor+\lfloor y/2^i\rfloor = \lfloor(3L-1)/2^i\rfloor = 3\cdot 2^{m-i}-1\) 为常数。

因此这些 \((x,y)\) 对应同样的 \(f\) 值:

\[\begin{aligned} f(x,y) &= \sum_{i=0}^m (3\cdot 2^{m-i}-1)\binom{i+m}i\\ &= 3\cdot 4^m - \binom{2m+1}m. \end{aligned}\]

\(\displaystyle n=3\cdot 4^m - \binom{2m+1}m - 1\),则这些 \((x,y)\) 恰好都不符合条件。
且显然 \(f(x,y)\) 关于 \(y\) 是严格递增的,那么将 \(y\) 减小 \(1\) 后一定都符合条件。

因此对于 \(L\le x\le 2L-1\),有 \(Y(x)=3L-2-x\)
也就是这一段上每个 \(x\) 都对应不同的 \(Y(x)\),共有 \(L=O(\sqrt n)\) 种不同的取值。
即使只看 \(x<y\) 的部分,也只是让取值个数减半,量级仍为 \(O(\sqrt n)\)

这意味着块的数量能被卡到 \(O(\sqrt n)\),总复杂度仍为 \(O(\sqrt n \log n)\)
所以这种方法并不能改进复杂度。

Code

\(O(\sqrt n\log n)\) 做法、带分块的实现。

#include<bits/stdc++.h>
typedef long long i64;

i64 n, C[100][100], w[100];

i64 calc(i64 x) {
    int A = std::__lg(x);
    int B = std::__lg(2 * n / x);
    w[0] = C[A][0];
    for (int j = 1; j <= B; ++j)
        w[j] = 2 * w[j - 1] + C[j + A][j];
    i64 s = 0;
    for (int cnt = 1; cnt <= 3; ++cnt) {
        for (int i = 0; i <= A; ++i)
            s += (x >> i) * C[i + B][i];
        if (!B || s + w[B] <= n) break;
        s = 0;
        --B;
    } // 微调 B
    i64 y = 0;
    for (int j = B; ~j; --j)
        if (s + w[j] <= n) {
            s += w[j];
            y += (1ll << j);
        }
    return y;
}

int main() {
    scanf("%lld", &n);
    int k = std::__lg(n) + 2;
    for (int i = 0; i < k; ++i) {
        C[i][0] = 1;
        for (int j = 1; j <= i; ++j)
            C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
    }
    i64 ans = 2 * n + 1;
    i64 x = 1, y;
    while ((y = calc(x)) >= x) {
        i64 x0 = x;
        x = calc(y) + 1;
        ans += (y - x + y - x0 + 2) * (x - x0);
    } // 分块计算
    printf("%lld\n", ans);
    return 0;
}
posted @ 2026-06-19 16:58  icyM3tra  阅读(35)  评论(0)    收藏  举报