EOJ-2020“游族杯”C题 Coronavirus Battle (CDQ分治、枚举优化)
题目链接:EOJ-2020“游族杯”C题 Coronavirus Battle
题意
有 \(n(1\leq n\leq 10^5)\) 个细胞,每个细胞各有一个三维坐标表示其位置,坐标由给定随机种子的伪随机数产生(随机数的范围是 unsigned long long)。病毒会对细胞进行多轮攻击,在第 \(k\) 轮攻击中,细胞 \(i\) 能够存活当且仅当存在细胞 \(j\) 满足 \(x_j\leq x_i \&\& y_j\leq y_i \&\& z_j\leq z_i \&\& (x_j<x_i || y_j<y_i||z_j<z_i)\) ,其中细胞 \(j\) 要满足病毒第 \(k-1\) 轮攻击后仍存活。求病毒攻击几轮之后能消灭所有细胞,和每个细胞各能存活几轮。
思路
解法一:枚举优化
首先容易想到三重循环的暴力解法,枚举轮数、枚举各个细胞 \(i\),枚举各个细胞 \(j\) 判断 \(j\) 能否保护 \(i\) 。
由于坐标由随机数产生,所以细胞在坐标系上的分布是均匀的,可以发现最多攻击约 100 轮就能消灭所有细胞。但这样三重循环仍然很慢,考虑优化第二、三重循环的枚举:
- 上一轮病毒攻击后已经被消灭的细胞对后面都没有作用了,直接剔除出去,不参与下一轮的枚举;
- 对三维坐标的其中一个维度排序,比如对 \(x\) 轴的坐标从小到大排序,那么细胞 \(j\) 能够保护细胞 \(i\) 由于要满足 \(x_j\leq x_i\) ,可得 \(j<i\) ,所以第三重循环的上界为 \(i\) 即可(这里说的 \(i,j\) 是排序后的细胞新编号)。但即使第三重循环的上界为 \(i\) 仍然不够优秀,我们思考如果 \(j\) 能保护 \(i\) 并且 \(j\) 能在这一轮后存活,那么保护 \(j\) 的细胞 \(j_1\) 其实也能够保护 \(i\) ;如果 \(j_1\) 能存活,那么保护 \(j_1\) 的细胞 \(j_2\) 也能保护 \(i\) ...... 这样一重重套下去,最终肯定有一个在这一轮后不能存活的细胞 \(j_k\) 能够保护 \(i\) ,且 \(j_k<...<j_2<j_1<j<i\) 。所以第三重循环只要枚举在这一轮已死的细胞即可,不用枚举能存活的细胞。
- 由于随机数的范围很大,所以基本不可能随机出相同的数字,所以 \(x_j\leq x_i \&\& y_j\leq y_i \&\& z_j\leq z_i \&\& (x_j<x_i || y_j<y_i||z_j<z_i)\) 也等价于 \(x_j<x_i \&\& y_j<y_i \&\& z_j<z_i\) 。因为这条逻辑表达式是放在最内层循环中,是程序中被执行次数最多的语句,所以这样改后能优化一些时间。
解法二:cdq分治
前置知识:cdq分治
观察到 \(x_j<x_i \&\& y_j<y_i \&\& z_j<z_i\) 这个形式和 三维偏序 问题十分相似,三维偏序问题就是 cdq 分治的典型问题。只是三维偏序问题求的是满足表达式的元素数量,而这道题并不是有多少个 \(j\) 能保护 \(i\) ,\(i\) 就能存活多少轮。我们用 \(j_k\to j_{k-1}\to...j_2\to j_1\) 来表示细胞 \(j_k\) 能保护 \(j_{k-1}\) ,\(j_{k-1}\) 能保护 \(j_{k-2}\) ...... \(j_2\) 能保护 \(j_1\) 这样一条保护链,那么 \(j_1\) 能够存活几轮就取决于这条链的长度。实际情况不是简单的链形关系,而是“有向图”形关系,如果说三维偏序问题各元素建模成“有向图”形关系后是求每个结点的前缀结点数量,那么这道题就是求每个结点的最长前缀链长度。这里只是形象化的理解,不是真要构建有向图来求解,这对于求解问题的时间空间复杂度并没有帮助。
从求数量变成求最长,cdq 分治中的树状数组维护前缀和就要变成维护前缀最大长度值。通常,cdq 分治都是先处理完 \([l,mid]\) 和 \([mid+1,r]\) ,再归并处理 \([l,r]\) ,在这道题中,\([l,r]\) 要在 \([mid+1,r]\) 之前处理。比如 \(1\to 2\to3\to 4\) ,先处理 \([1,2]\) ,能知道 \(2\) 有 \(1\to 2\) 这条长度为 2 的前缀链,再处理 \([3,4]\) ,能知道 \(4\) 有 \(3\to 4\) ,最后处理 \([1,4]\) ,只能知道 \(4\) 还有 \(1\to 2\to 4\) 这条长度为 3 的前缀链;如果先处理 \([1,4]\) ,就能更新 \(3\) 的前缀链长度为 3,再处理 \([3,4]\) ,能用 \(3\) 去更新 \(4\) 的前缀链长度为 4 。时间复杂度 \(O(nlog^2n)\) 。
代码实现
枚举优化:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 100010;
struct node {
int id;
unsigned long long x, y, z;
} cell[maxn];
int ans[maxn];
unsigned long long k1, k2;
unsigned long long CoronavirusBeats() {
unsigned long long k3 = k1, k4 = k2;
k1 = k4;
k3 ^= k3 << 23;
k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
return k2 + k4;
}
bool cmp(const node &a, const node &b) {
if (a.x != b.x) return a.x < b.x;
if (a.y != b.y) return a.y < b.y;
return a.z < b.z;
}
int main() {
int n;
cin >> n >> k1 >> k2;
for (int i = 0; i < n; i++) {
cell[i].x = CoronavirusBeats();
cell[i].y = CoronavirusBeats();
cell[i].z = CoronavirusBeats();
cell[i].id = i;
}
int lun;
sort(cell, cell+ n, cmp);
vector<int> sur; // 目前还存活的细胞
for (int i = 0; i < n; i++) sur.push_back(i);
for (lun = 1; sur.size() > 0; lun++) { // 枚举每一轮
vector<int> die, tmp_sur; // die表示在这一轮被消灭的细胞
die.push_back(sur[0]); // sur[0]是x坐标最小的细胞,肯定没被保护
ans[cell[sur[0]].id] = lun - 1;
for (int i = 1; i < sur.size(); i++) {
int ii = sur[i];
bool flag = false; // 细胞i能否在这一轮存活
/* 由于按坐标排序了,如果细胞i能在这一轮存活,那么能保护i的所有细胞中
肯定有这一轮已死的,逆否命题就是如果这一轮已死的所有细胞都不能保护i,
那么i也不能存活 */
for (int j : die) {
if (cell[j].x < cell[ii].x || cell[j].y < cell[ii].y || cell[j].z < cell[ii].z) {
flag = true;
break;
}
}
if (flag) tmp_sur.push_back(ii);
else {
ans[cell[ii].id] = lun - 1;
die.push_back(ii);
}
}
sur = tmp_sur;
}
printf("%d\n", lun - 1);
for (int i = 0; i < n; i++) printf("%d%c", ans[i], " \n"[i==n]);
return 0;
}
cdq分治:
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 100010;
struct node {
int id;
unsigned long long x, y, z;
} cell[maxn], tmp_c[maxn];
int ans[maxn], tr[maxn], n;
unsigned long long tmp[maxn];
unsigned long long k1, k2;
unsigned long long CoronavirusBeats() {
unsigned long long k3 = k1, k4 = k2;
k1 = k4;
k3 ^= k3 << 23;
k2 = k3 ^ k4 ^ (k3 >> 17) ^ (k4 >> 26);
return k2 + k4;
}
bool cmpx(const node &a, const node &b) {
return a.x < b.x;
}
bool cmpy(const node &a, const node &b) {
return a.y < b.y;
}
void change() { // 对z轴的值离散化
for (int i = 0; i < n; i++) tmp[i] = cell[i].z;
sort(tmp, tmp + n);
for (int i = 0; i < n; i++) cell[i].z = lower_bound(tmp, tmp + n, cell[i].z) - tmp + 1;
}
int lowbit(int x) {
return x & -x;
}
void update(int x, int val) {
while (x <= n) tr[x] = max(tr[x], val), x += lowbit(x);
}
int query(int x) {
int res = 0;
while (x) res = max(res, tr[x]), x -= lowbit(x);
return res;
}
void recover(int x) {
while (x <= n) tr[x] = 0, x += lowbit(x);
}
void cdq_solve(int l, int r) {
int mid = (l + r) >> 1;
for (int i = l; i <= mid; i++) tmp_c[i] = cell[i];
for (int i = mid + 1; i <= r; i++) tmp_c[i] = cell[i];
sort(tmp_c + l, tmp_c + mid + 1, cmpy);
sort(tmp_c + mid + 1, tmp_c + r + 1, cmpy);
int j = l;
for (int i = mid + 1; i <= r; i++) {
for (; j <= mid && tmp_c[j].y < tmp_c[i].y; j++) {
update(tmp_c[j].z, ans[tmp_c[j].id]);
}
ans[tmp_c[i].id] = max(ans[tmp_c[i].id], query(tmp_c[i].z) + 1);
}
int delj = j;
for (int i = l; i < delj; i++) recover(tmp_c[i].z);
}
void cdq(int l, int r) {
if (l == r) return ;
int mid = (l + r) >> 1;
cdq(l, mid);
cdq_solve(l, r);
cdq(mid + 1, r);
}
int main() {
scanf("%d %llu %llu", &n, &k1, &k2);
for (int i = 0; i < n; i++) {
cell[i].x = CoronavirusBeats();
cell[i].y = CoronavirusBeats();
cell[i].z = CoronavirusBeats();
cell[i].id = i;
ans[i] = 1;
}
sort(cell, cell+ n, cmpx);
change();
cdq(0, n - 1);
printf("%d\n", *max_element(ans, ans + n));
for (int i = 0; i < n; i++) printf("%d%c", ans[i] - 1, " \n"[i==n-1]);
return 0;
}