[CF1550F]Jumping Around
壹、题目描述 ¶
贰、题解 ¶
首先明白,如果你想从 \(i\) 跳到 \(j\),那么你需要的修正值为
如果我们想从 \(s\) 走到 \(t\),假设我们经过的路径为 \(\{p_0,p_1,p_2,...,p_x|p_0=s,p_x=t\}\),那么我们需要的修正值即为
然后比较 \(req_t\) 与它给的 \(k\) 即可,如果 \(k<req_t\),那么答案为 \(\tt nO\),否则为 \(\tt yEs\).
现在问题变成,如何求 \(req_t\)?
我们可以将 \(w_{ij}\) 看成一条边,那么,现在我们面临的就是一张有 \(n\) 个点,\(n(n-1)\over 2\) 条边的图,现在,我们想要从给定起点 \(s\) 到某个点的经过的最大边最小,这不难让我们想到了二分答案。
不过,这又是另一个问题 —— 二分答案之后,如何检查?一个比较普遍的想法就是,将所有权小于等于 \(mid\) 的边连接的两个点暴力合并到一个并查集中,最后看 \(s,t\) 是否在同一个并查集中即可。
但是这样做,似乎二分答案有点多于,我们可以直接贪心地将当前权最小的边连接的俩点加入同一个并查集中,某个时刻 \(s,t\) 位于同一个并查集时,最后加入的这条边就是答案。
但是这样做有两个问题:
- 边最多达到 \(n^2\) 级别;
- 每个询问都反复加边实在太重复;
我们重新思考上面的过程,发现它似乎和最小生成树比较相似,我们从生成树方向考虑。
我们将第一次连接两个并查集的边保留,不难发现它最后就会是一个最小生成树,而从 \(s\) 到某个 \(t\) 所需的修正值就是树上从 \(s\) 到 \(t\) 的路径中经过的最大边的权。
于是,我们现在的目的是 —— 如何快速地求出原图的 “最小生成树”;
如果是使用普遍的 \(\rm Prim\) 或者 \(\rm Kruskal\) 似乎都比较困难,前者因为复杂度为 \(\mathcal O((\siz V+\siz E)\log \siz V)\),而 \(m\) 较大,后者也是因为需要对 \(\siz E={n(n-1)\over 2}\) 条边进行排序,复杂度也不那么好看,我们得找一种更先进的,可以将边的数量降下来的方法。
此处,我们需要一种名为 \(\rm Boruvka\) 的最小生成树算法,它的复杂度为 \(\mathcal O(\siz E\log \siz V)\),但是它的好处在于 —— 在已知什么边最优的情况,我们可以不用将所有的边都求出来。更强地,它甚至可以解决一些二进制生成树问题,而广大 \(\rm OIer\) 都深感其优势。
(不知道如何打码,就手动上一个了)
其具体过程可以这样描述:(以下内容摘自 OI Wiki)
为了描述该算法,我们需要引入一些定义:
- 定义 \(E'\) 为我们当前找到的最小生成森林的边。在算法执行过程中,我们逐步向 \(E'\) 加边,定义 连通块 表示一个点集 \(V'\subseteq V\),且这个点集中的任意两个点 \(u,v\), 在 \(E'\) 中的边构成的子图上是连通的(互相可达)。
- 定义一个连通块的 最小边 为它连向其它连通块的边中权值最小的那一条。
初始时,\(E'=\emptyset\),每个点各自是一个连通块:
- 计算每个点分别属于哪个连通块。将每个连通块都设为“没有最小边”。
- 遍历每条边 \((u,v)\),如果 \(u\) 和 \(v\) 不在同一个连通块,就用这条边的边权分别更新 \(u\) 和 \(v\) 所在连通块的最小边。
- 如果所有连通块都没有最小边,退出程序,此时的 \(E'\) 就是原图最小生成森林的边集。否则,将每个有最小边的连通块的最小边加入 \(E'\),返回第一步。
下面是伪代码:
\[\begin{array}{ll} 1 & \textbf{Input. } \text{A graph }G\text{ whose edges have distinct weights. } \\ 2 & \textbf{Output. } \text{The minimum spanning forest of }G . \\ 3 & \textbf{Method. } \\ 4 & \text{Initialize a forest }F\text{ to be a set of one-vertex trees} \\ 5 & \textbf{while } \text{True} \\ 6 & \qquad \text{Find the components of }F\text{ and label each vertex of }G\text{ by its component } \\ 7 & \qquad \text{Initialize the cheapest edge for each component to "None"} \\ 8 & \qquad \textbf{for } \text{each edge }(u, v)\text{ of }G \\ 9 & \qquad\qquad \textbf{if } u\text{ and }v\text{ have different component labels} \\ 10 & \qquad\qquad\qquad \textbf{if } (u, v)\text{ is cheaper than the cheapest edge for the component of }u \\ 11 & \qquad\qquad\qquad\qquad\text{ Set }(u, v)\text{ as the cheapest edge for the component of }u \\ 12 & \qquad\qquad\qquad \textbf{if } (u, v)\text{ is cheaper than the cheapest edge for the component of }v \\ 13 & \qquad\qquad\qquad\qquad\text{ Set }(u, v)\text{ as the cheapest edge for the component of }v \\ 14 & \qquad \textbf{if }\text{ all components'cheapest edges are "None"} \\ 15 & \qquad\qquad \textbf{return } F \\ 16 & \qquad \textbf{for }\text{ each component whose cheapest edge is not "None"} \\ 17 & \qquad\qquad\text{ Add its cheapest edge to }F \\ \end{array} \]
十分佩服 \(\text{OI Wiki}\) 的同志直接使用 \(\LaTeX\) 而不用任何宏包书写伪代码,这让我有了共产主义的空间。
值得注意的是,在伪代码中,有两句判断是
如果我们可以直接找到这样的边,那是最好不过。
显然,对于一个点 \(u\),如果它连接的另一个集合的点与 \(a_u\pm d\) 差最小,那么所需的修正值也越小,那么我们可以直接使用 \(\tt lower\_bound()\) 在一个储存了坐标的 \(\tt set\) 中解决问题,这样我们就不需要枚举一个点连出去的所有边了。
然后,我们就可以实现代码了,代码中有一些细节,自己康康罢。
该算法的复杂度应该是 \(\mathcal O(\siz V\log^2\siz V)\) 的罢。当然,如果你可以不使用 \(\tt set\),应该是可以降低至 \(\mathcal O(\siz V\log \siz V)\) 的。注意,启发式合并的 \(\mathcal O(\siz V\log \siz V)\) 是独立的,不和 \(\rm Boruvka\) 算法的主体部分相乘。至于你问哪里用到了启发式合并,看看代码罢。
叁、参考代码 ¶
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
using namespace std;
// #define NDEBUG
#include<cassert>
namespace Elaina{
#define rep(i, l, r) for(int i=(l), i##_end_=(r); i<=i##_end_; ++i)
#define drep(i, l, r) for(int i=(l), i##_end_=(r); i>=i##_end_; --i)
#define fi first
#define se second
#define mp(a, b) make_pair(a, b)
#define Endl putchar('\n')
#define mmset(a, b) memset(a, b, sizeof a)
// #define int long long
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
template<class T>inline T fab(T x){ return x<0? -x: x; }
template<class T>inline void getmin(T& x, const T rhs){ x=min(x, rhs); }
template<class T>inline void getmax(T& x, const T rhs){ x=max(x, rhs); }
template<class T>inline T readin(T x){
x=0; int f=0; char c;
while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
return f? -x: x;
}
template<class T>inline void writc(T x, char s='\n'){
static int fwri_sta[1005], fwri_ed=0;
if(x<0) putchar('-'), x=-x;
do fwri_sta[++fwri_ed]=x%10, x/=10; while(x);
while(putchar(fwri_sta[fwri_ed--]^48), fwri_ed);
putchar(s);
}
}
using namespace Elaina;
const int maxn=2e5;
const int inf=0x3f3f3f3f;
struct edge{
int u, v, w;
inline bool operator <(const edge rhs) const{
return w<rhs.w;
}
};
set<int>plane;
vector< vector<int> >compo;
vector<int>a, fa, refl;
inline int findrt(int u){
return fa[u]==u? u: fa[u]=findrt(fa[u]);
}
inline bool merge(int u, int v){
u=findrt(u), v=findrt(v);
if(u==v) return 0;
if(compo[u]<compo[v]) swap(u, v);
for(int x: compo[v]){
compo[u].push_back(x);
fa[refl[x]]=u;
}
compo[v].clear();
return 1;
}
int n, q, s, d;
inline void input(){
n=readin(1), q=readin(1), s=readin(1)-1, d=readin(1);
fa.resize(n), a.resize(n);
for(int i=0; i<n; ++i){
fa[i]=i, a[i]=readin(1);
plane.insert(a[i]);
}
}
vector<pii>g[maxn+5]; int req[maxn+5];
inline void add_edge(int u, int v, int w){
g[u].push_back({v, w}); g[v].push_back({u, w});
}
void dfs(int u, int par, int mx){
req[u]=mx;
for(pii e: g[u]) if(e.fi!=par)
dfs(e.fi, u, max(mx, e.se));
}
signed main(){
input();
compo.resize(n), refl=vector<int>(a[n-1]+1);
for(int i=0; i<n; ++i){
compo[i]=vector<int>(1, a[i]);
refl[a[i]]=i;
}
int cnt=n;
while(cnt>1){
vector<edge>Es; edge mn;
for(auto cur: compo) if(!cur.empty()){
mn={-1, -1, inf};
for(int x: cur) plane.erase(x);
for(int x: cur){
for(int dir: {-d, d}){
auto it=plane.lower_bound(x+dir);
if(it!=plane.end()) mn=min(mn, {refl[x], refl[*it], fab(d-fab(x-(*it)))});
if(it!=plane.begin()){
--it;
mn=min(mn, {refl[x], refl[*it], fab(d-fab(x-(*it)))});
}
}
}
for(int x: cur) plane.insert(x);
Es.push_back(mn);
}
for(auto e: Es) if(merge(e.u, e.v))
--cnt, add_edge(e.u, e.v, e.w);
}
dfs(s, -1, -1);
while(q--){
int i=readin(1)-1, k=readin(1);
if(req[i]<=k) printf("yEs\n");
else printf("nO\n");
}
return 0;
}
肆、一些无关紧要的东西 ¶
还记得上文提到的 更强地,它甚至可以解决一些二进制生成树问题 吗?这里有一道题,就是这样的问题(当然被一些妹妹用子集切掉了)。
伍、关键之处 ¶
其实 \(\rm Boruvka\) 学了都会,主要是意识到 \(w_{ij}=|d-|a_i-a_j||\).