P1081 题解
前言
本文中的对数函数默认底数为
2025 年 5 月 17 日,发现代码贴错了,已改正。
2025 年 5 月 17 日,人名字体更改了。
2025 年 5 月 18 日,时间复杂度的符号更改了。
题意概述
有
问:
- 对于给定的
x ,从哪个城市出发小 A 行驶的总距离与小 B 行驶的总距离的比值最小(若小 B 行驶的总距离为0 ,则比值为无穷大;若多个比值相等,则答案为海拔最高的城市)?(共询问1 次) - 对于给定的出发城市
s 与x ,求小 A 行驶的总距离与小 B 行驶的总距离。(共询问m(1 \le m \le 10^5) 次)
想看题目点这里:题目链接。
正解思路
首先,这道题是一道静态查询的题目,解决这类问题有一个很好的方法——预处理。很显然,要想解决这道题,小 A 和小 B 的行驶策略是很重要的。我们需要知道,已知当前的城市,小 A 和小 B 行驶到的下一个城市是哪一座以及这次行驶的距离(先不管得知之后该如何用,至少在目前的思路还是需要知道的)?
为了解决这个问题,首先就需要了解小 A 与小 B 的策略。
小 A 与 小 B 的行驶策略
先看小 B,小 B 总是开往前进方向上第一近的城市编号。即:若当前城市编号为
更进一步,这样的
再看小 A,同理可得:寻找小 A 行驶的下一个城市,就需要寻找满足
输入及预处理小 A 与小 B 行驶的下一个城市
方法是有了,但是要如何去做呢?输入完了之后,如果直接循环
我们发现,当
这部分只剩下一件事了:如何常数级地找到原来下标
所以,这部分的时间复杂度就是:输入
贴上相关部分代码:
cin >> n;
for(int i = 1; i <= n; i ++) {
cin >> h[i].hh;
h[i].ix = i;
}
sort(h + 1, h + n + 1, cmp1);//对 h 数组排序
for(int i = 1; i <= n; i ++) {
h[i].prv = i - 1;
h[i].nxt = i + 1;
ixh[h[i].ix] = i;//方便 O(1) 找到原数组编号为 h[i].ix 的元素在排序后数组的下标(i)
}
h[n].nxt = 0;
for(int i = 1; i <= n; i ++) {
top = 0;
temp = ixh[i];
Work(h[temp].prv);
Work(h[h[temp].prv].prv);
Work(h[temp].nxt);
Work(h[h[temp].nxt].nxt);
sort(t + 1, t + top + 1, cmp2);//排序
if(top >= 1) {//至少有一个可以到达的城市,则小 B 的策略可以满足
b[i] = h[t[1]].ix;
disb[i] = Abs(h[t[1]].hh - h[temp].hh);
}
if(top >= 2) {//至少有两个可以到达的城市,则小 A 的策略可以满足
a[i] = h[t[2]].ix;
disa[i][0] = Abs(h[t[2]].hh - h[temp].hh);
}
if(h[temp].nxt) h[h[temp].nxt].prv = h[temp].prv;
if(h[temp].prv) h[h[temp].prv].nxt = h[temp].nxt;
}
相关函数:
int Abs(int u) {
return (u > 0) ? u : (-u);
}
bool cmp1(node u, node v) {//排序比较函数 1
return u.hh < v.hh;//按高度从小往大排序
}
bool cmp2(int u, int v) {//排序比较函数 2
if(Abs(h[u].hh - h[temp].hh) != Abs(h[v].hh - h[temp].hh)) return Abs(h[u].hh - h[temp].hh) < Abs(h[v].hh - h[temp].hh);//如果差的绝对值不相等,就按差的绝对值从小往大排序
return h[u].hh < h[v].hh;//否则,高度小的距离更近,按高度从小往大(虽然最多只有两个)
}
void Work(int u) {
if(u) t[++top] = u;
}
相关数据结构:
int n, ixh[100010], t[100010], top, temp, a[100010], disa[100010][17], b[100010], disb[100010];
struct node {
int hh, ix, prv, nxt;//高度、原数组下标、前驱、后继
}h[100010];
计算倍增数组
那接下来,该如何用呢?假设在城市
接下来考虑优化,一组一组地行驶太慢了,能不能使用二进制的方式从大往小枚举呢?但是这样就需要预处理从城市
相应地,
当然,以上
但是我们发现,使用的时候需要同时求出小 A 与小 B 的行驶路程,但是显然小 A 行驶的路程加上小 B 行驶的流程就是他们两个总共行驶的路程。所以只需要存小 A 的路程就足够了(当然,你要存小 B 的也不是不可以)。改一下定义。原来的
时间复杂度显然就是
贴上相关部分代码:
for(int i = 1; i <= n; i ++) {
ab[i][0] = b[a[i]];
disab[i][0] = disa[i][0] + disb[a[i]];
}
k = log2(n);//提前存储一下
for(int j = 1; j <= k; j ++) {
for(int i = 1; i <= n; i ++) {
ab[i][j] = ab[ab[i][j - 1]][j - 1];
disab[i][j] = disab[i][j - 1] + disab[ab[i][j - 1]][j - 1];
disa[i][j] = disa[i][j - 1] + disa[ab[i][j - 1]][j - 1];
}
}
相关数据结构:
int n, k, a[100010], disa[100010][17], b[100010], disb[100010], ab[100010][17], disab[100010][17];
回答询问
倍增数组都预处理完了,接下来就可以回答询问了。其实前面也提到过回答询问的方法。其实询问核心只有一种,就是给定出发城市
先看问题二,其实倍增数组预处理完了之后就很简单了,只要从大往小尝试行驶的组数以二为底数的幂指数,满足条件就行驶,累加总路程与小 A 行驶的路程,最后记得特判小 A 单独再行驶一天的情况,总路程减去小 A 行驶的路程就是小 B 行驶的路程。
那么问题一就很简单了,枚举出发城市 double 或 long double 来存储)后比较大小,另一种是分子与分母分别交叉相乘,比较大小。但是第二种方式的乘积可能要用 int128 的类型来存储,因为乘积的极限是 long long 存不下(说起存储,温馨提示:本题建议大部分变量用 long long 存储)。一定要注意,不论是哪一种方法,都要先判断分母是
那时间复杂度就是:问题一
最后,总的时间复杂度就是
贴上相关部分代码:
cin >> x;
for(int i = 1; i <= n; i ++) {
Calc(i, x);
if(i == 1) Ans(i, lena, lenb);//目前还没有任何答案,直接记录下来
else {
if(!lenb) {
if((!ansb) && h[ixh[i]].hh > h[ixh[ansi]].hh) Ans(i, lena, lenb);//两个比值都是无穷大,比高度
}
else {
if(!ansb) Ans(i, lena, lenb);//新的比值不是无穷大,而目前的答案的比值是无穷大
else {//两个比值都不是无穷大
__int128 answ = __int128(ansa) * __int128(lenb);
__int128 lenw = __int128(lena) * __int128(ansb);
if(lenw < answ) Ans(i, lena, lenb);//新的比值比目前的答案的比值更小
else if(lenw == answ && h[ixh[i]].hh > h[ixh[ansi]].hh) Ans(i, lena, lenb);//两个比值相等,比高度
}
}
}
}
cout << ansi << endl;
cin >> m;
while(m --) {
cin >> s >> x;
Calc(s, x);
cout << lena << " " << lenb << endl;
}
相关函数:
void Calc(int ss, int xx) {//计算从城市 ss 出发,在总路程不超过 xx 的情况下小 A 行驶的路程与小 B 行驶的路程
lena = lenb = 0;//小 A 的路程与小 B 的路程(先计算一共的路程,再减去小 A 的路程得出小 B 的路程)初值为 0
for(int i = k; i >= 0; i --) {
if(ab[ss][i] && lenb + disab[ss][i] <= xx) {
lenb += disab[ss][i];
lena += disa[ss][i];
ss = ab[ss][i];
}
}
if(a[ss] && lenb + disa[ss][0] <= xx) {//特判小 A 最后单独行驶一天的情况
lenb += disa[ss][0];
lena += disa[ss][0];
ss = a[ss];
}
lenb -= lena;//求出小 B 的路程
}
void Ans(int u, int v, int w) {//记录答案
ansi = u;
ansa = v;
ansb = w;
}
相关数据结构:
int n, k, ixh[100010], a[100010], disa[100010][17], ab[100010][17], disab[100010][17], m, x, s, lena, lenb, ansa, ansb, ansi;
思路梳理
最后梳理一下思路:
-
预处理
- 输入
n 并循环输入原数组,保留原数组下标,按高度排序 - 用数组模拟双向链表,并存储原数组元素在排序后数组中的下标
- 循环从
1 到n 枚举原数组的下标i - 找到原数组中下标为
i 的元素在排序后数组中的下标 - 找到它在链表中的前驱、前驱的前驱、后继、后继的后继(如果有的话),并对找到它们中距离循环枚举的城市最短和次短的城市(如果有的话),并将这个城市与其与循环枚举的城市的距离存储下来
- 将循环所枚举的元素从用数组模拟的链表中删除
- 循环城市编号
i ,从1 到n - 处理出
ab_{i,0}=b_{a_i} ,disab_{i,0}=disa_{i,0}+disb_{a_i} - 循环倍增数组中的第二维
j ,从1 到\lfloor \log n \rfloor - 循环倍增数组的第一维
i ,从1 到n - 处理出
disa_{i,j}=disa_{i,j-1}+disa_{ab_{i,j-1},j-1} ,ab_{i,j} = ab_{ab_{i,j-1},j-1} ,disab_{i,j} = disab_{i,j-1}+disab_{ab_{i,j-1},j-1}
- 处理出
- 输入
-
回答询问
- 问题一
- 输入
x - 循环枚举出发城市
i - 循环枚举行驶的组数的幂指数(底数为
2 ) - 尝试两人行驶
2^j 组(在满足条件的情况下),并记录小 A 的行驶路程与两人的行驶路程 - 特判小 A 单独行驶一天的情况
- 计算小 B 行驶的路程,即两人的行驶路程减去小 A 的行驶路程
- 按题目要求比较答案,与现有答案比较,选出使小 A 与小 B 行驶的路程的比值最小的出发城市(若相等,则比较高度),如果是这次新的答案,就将其记录在现有的答案中
- 循环枚举行驶的组数的幂指数(底数为
- 问题二
- 共有
m 次询问,循环m 次- 循环枚举行驶的组数的幂指数(底数为
2 ) - 尝试两人行驶
2^j 组(在满足条件的情况下),并记录小 A 的行驶路程与两人的行驶路程 - 特判小 A 单独行驶一天的情况
- 输出答案
代码
我们发现在思路中,有一些重复的步骤,比如:
- 循环枚举行驶的组数的幂指数(底数为
-
循环枚举行驶的组数的幂指数(底数为
2 )- 尝试两人行驶
2^j 组(在满足条件的情况下),并记录小 A 的行驶路程与两人的行驶路程
- 尝试两人行驶
-
特判小 A 单独行驶一天的况
所以,可以将这样的步骤封装成一个函数。
好了,贴代码。
#include <bits/stdc++.h>
#define int long long
using namespace std;
int n, k, ixh[100010], t[100010], top, temp, a[100010], disa[100010][17], b[100010], disb[100010], ab[100010][17], disab[100010][17], m, x, s, lena, lenb, ansa, ansb, ansi;
struct node {
int hh, ix, prv, nxt;//高度、原数组下标、前驱、后继
}h[100010];
int Abs(int u) {
return (u > 0) ? u : (-u);
}
bool cmp1(node u, node v) {//排序比较函数 1
return u.hh < v.hh;//按高度从小往大排序
}
bool cmp2(int u, int v) {//排序比较函数 2
if(Abs(h[u].hh - h[temp].hh) != Abs(h[v].hh - h[temp].hh)) return Abs(h[u].hh - h[temp].hh) < Abs(h[v].hh - h[temp].hh);//如果差的绝对值不相等,就按差的绝对值从小往大排序
return h[u].hh < h[v].hh;//否则,高度小的距离更近,按高度从小往大(虽然最多只有两个)
}
void Work(int u) {
if(u) t[++top] = u;
}
void Calc(int ss, int xx) {//计算从城市 ss 出发,在总路程不超过 xx 的情况下小 A 行驶的路程与小 B 行驶的路程
lena = lenb = 0;//小 A 的路程与小 B 的路程(先计算一共的路程,再减去小 A 的路程得出小 B 的路程)初值为 0
for(int i = k; i >= 0; i --) {
if(ab[ss][i] && lenb + disab[ss][i] <= xx) {
lenb += disab[ss][i];
lena += disa[ss][i];
ss = ab[ss][i];
}
}
if(a[ss] && lenb + disa[ss][0] <= xx) {//特判小 A 最后单独行驶一天的情况
lenb += disa[ss][0];
lena += disa[ss][0];
ss = a[ss];
}
lenb -= lena;//求出小 B 的路程
}
void Ans(int u, int v, int w) {//记录答案
ansi = u;
ansa = v;
ansb = w;
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++) {
cin >> h[i].hh;
h[i].ix = i;
}
sort(h + 1, h + n + 1, cmp1);//对 h 数组排序
for(int i = 1; i <= n; i ++) {
h[i].prv = i - 1;
h[i].nxt = i + 1;
ixh[h[i].ix] = i;//方便 O(1) 找到原数组编号为 h[i].ix 的元素在排序后数组的下标(i)
}
h[n].nxt = 0;
for(int i = 1; i <= n; i ++) {
top = 0;
temp = ixh[i];
Work(h[temp].prv);
Work(h[h[temp].prv].prv);
Work(h[temp].nxt);
Work(h[h[temp].nxt].nxt);
sort(t + 1, t + top + 1, cmp2);//排序
if(top >= 1) {//至少有一个可以到达的城市,则小 B 的策略可以满足
b[i] = h[t[1]].ix;
disb[i] = Abs(h[t[1]].hh - h[temp].hh);
}
if(top >= 2) {//至少有两个可以到达的城市,则小 A 的策略可以满足
a[i] = h[t[2]].ix;
disa[i][0] = Abs(h[t[2]].hh - h[temp].hh);
}
if(h[temp].nxt) h[h[temp].nxt].prv = h[temp].prv;
if(h[temp].prv) h[h[temp].prv].nxt = h[temp].nxt;
}
for(int i = 1; i <= n; i ++) {
ab[i][0] = b[a[i]];
disab[i][0] = disa[i][0] + disb[a[i]];
}
k = log2(n);//提前存储一下
for(int j = 1; j <= k; j ++) {
for(int i = 1; i <= n; i ++) {
ab[i][j] = ab[ab[i][j - 1]][j - 1];
disab[i][j] = disab[i][j - 1] + disab[ab[i][j - 1]][j - 1];
disa[i][j] = disa[i][j - 1] + disa[ab[i][j - 1]][j - 1];
}
}
cin >> x;
for(int i = 1; i <= n; i ++) {
Calc(i, x);
if(i == 1) Ans(i, lena, lenb);//目前还没有任何答案,直接记录下来
else {
if(!lenb) {
if((!ansb) && h[ixh[i]].hh > h[ixh[ansi]].hh) Ans(i, lena, lenb);//两个比值都是无穷大,比高度
}
else {
if(!ansb) Ans(i, lena, lenb);//新的比值不是无穷大,而目前的答案的比值是无穷大
else {//两个比值都不是无穷大
__int128 answ = __int128(ansa) * __int128(lenb);
__int128 lenw = __int128(lena) * __int128(ansb);
if(lenw < answ) Ans(i, lena, lenb);//新的比值比目前的答案的比值更小
else if(lenw == answ && h[ixh[i]].hh > h[ixh[ansi]].hh) Ans(i, lena, lenb);//两个比值相等,比高度
}
}
}
}
cout << ansi << endl;
cin >> m;
while(m --) {
cin >> s >> x;
Calc(s, x);
cout << lena << " " << lenb << endl;
}
return 0;
}
当然,在上面代码中,比较比值的部分你也可以这样写:
if(i == 1 || (lenb && ((!ansb) || (ansb && (lenw < answ || (lenw == answ && h[ixh[i]].hh > h[ixh[ansi]].hh)))) || ((!lenb) && (!ansb) && h[ixh[i]].hh > h[ixh[ansi]].hh))) Ans(i, lena, lenb);
我的提交记录
我的提交记录
后记
本题代码较长,实现细节较多,但还是自己手打,不要抄代码。
最后祝愿大家在美好的绿色方块中通过此题!