P9218 Apollo题解
题目分析
仔细阅读题目,可知题目要求的是对于每个 \(a_i\) 的 \(\sum\limits_{j=1}^ng(a_i,a_j)\) 。再结合 \(g(a,b)\) 的定义,可知,对于 \(a_i\) 来说,我们需要计算 \(a_i\) 与 \(a_1\sim a_n\) 构成的 \(n\) 组数对的 \(g(a_i,a_j)\) 的总和。对于 \(g(a,b)\) 的值,则是 \([\min(a,b),\max(a,b)]\) 中的最小 \(f(c)\) 的值,当前 \(c\in [\min(a,b),\max(a,b)]\)。对于 \(f(c)\) 的作用则是返回将小数 \(c\) 变成整数的乘 \(10\) 的次数,即 \(c\) 的小数部分的位数。题目描述的比较绕,需要耐心将公式之间的关系捋清楚。
接下来思考如何计算 \(g(a,b)\)。我们分情况进行讨论,为方便描述我们设 \(a\le b\)。
- 两个数的整数部分不同。
当整数部分不相同时,在 \([a,b]\) 中一定可以存在一个整数 \(c\) ,当 \(c\) 为整数时,\(f(c)=0\)。所以整数部分不相同,\(g(a,b)=0\)。
-
两个数的整数部分相同。当整数部分相同时,考虑小数部分。可以分成三种情况:
-
小数部分完全不一样。
如 \(11.123\) 和 \(11.231\) 对应的 \(c=11.2\),\(f(c)=1\)。也就是较小的数值保留1位小数再加 \(0.1\) 就好,这样乘 \(10^1\) 就能成为整数。
-
部分前缀相同。
如 \(11.12345\) 和 \(11.12388\) 对应的 \(c=11.1235\),\(f(c)=4\)。也就是先找 \(a\) 和 \(b\) 的最大公共前缀,再往后一位,找一个在两者范围内的数字即可,若小数点后最大公共前缀长度为 \(k\) 则乘 \(10^{k+1}\) 就能成为整数。
-
较小的数字是较大数字的前缀。
如 \(11.123\) 和 \(11.123456\) 对应的 \(c=11.123\),\(f(c)=3\)。也就是较小的那个数字的小数位数,若较小数字的小数位置为 \(k\) 则乘 \(10^k\) 就能成为整数。
-
此时我们可以发现,问题是围绕着公共前缀展开的。对于小数的公共前缀,我们可以将小数视为字符串进行存储与使用,就将问题转换了字符串的公共前缀问题,这类问题使我们联想到字典树,可以考虑使用字典树解决它。
以样例2为例,讲解字典树解法思路。
8
1.1145
1.114
1.1145
1.514
1.19198
1.1154
1.114
1.1145
下图为建立好的字典树。
查找数值 \(1.1145\) 的答案过程。
查找数值 \(1.114\) 的答案过程。
查找数值 \(1.514\) 的答案过程。
模拟过程中可发现,在遍历字典树的过程中,我们计算新增的不同前缀的元素个数与当前结尾的元素个数,它们成为整数的答案为当前小数位数。另外,在遍历结束后需要考虑和计算以当前数字为前缀且又比它长的数字个数,详细可查看寻找数值 \(1.114\) 的过程。
代码实现
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
const int N = 1e5 + 5;
const int M = 3e6 + 5;
int n, tot, zs;
string str[N];
struct node {
int son[11];
// 前缀相同的字符串个数、以该节点结尾的字符串个数、小数部分的位数
int num, end, dep;
} trie[M];
int toNum[256];
void init() {
for (int i = 0; i < 10; i++) {
toNum[i + '0'] = i;
}
toNum['.'] = 10;
}
void insert(string s) { // 字典树插入
int len = s.size();
int u = 0;
int dot = -1; // 小数点位置
trie[u].num++; // 记录字符串总数
for (int i = 0; i < len; i++) {
int ch = toNum[s[i]];
if (!trie[u].son[ch]) trie[u].son[ch] = ++tot;
u = trie[u].son[ch];
trie[u].num++;
if (ch == 10) dot = i; // 记录小数点的位置
if (dot != -1) trie[u].dep = i - dot; // 更新小数部分对应的位数
}
trie[u].end++;//记录该处结尾的数字个数
}
int findStr(string s) {
// 整数部分不一样,f(c)为0
// 整数部分相同时考虑小数部分的最大公共前缀
int len = s.size();
int sum = 0, re = trie[0].num; // re-剩余前缀相同的字符串数量
int u = 0;
for (int i = 0; i < len; i++) {
int ch = toNum[s[i]];
u = trie[u].son[ch];
// num=新增的前缀不同的字符串数量 + 以该节点结尾的字符串数量
int num = re - trie[u].num + trie[u].end;
sum += num * trie[u].dep; // 更新总和
re -= num;
}
// 处理前缀与s相同,但又比s长的字符串
sum += (trie[u].num - trie[u].end) * trie[u].dep;
return sum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
init();
string s;
char c;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> str[i];
insert(str[i]);
}
for (int i = 1; i <= n; i++) {
cout << findStr(str[i]) << '\n';
}
return 0;
}