关于我想了很久才想出这题咋做这档事 - 5
#0.0 目录
目录中以下题目皆可点击查看原题
#1.0 P1967 [NOIP2013 提高组] 货车运输
#1.1 变图为树
初次看到该题是本人在练习树剖的时候,当时拿过题面来二话不说就写了个树剖,然后发现 每个点之间道路不唯一 ....(当然树剖应该也可以做,不过不是上来不处理就剖)
不难想到,在走从 \(x\) 到 \(y\) 的某一条路径时,限制货车装载量的是该条路径上的最小限重,那我们就应该让所选取的路径上的最小限重尽可能大,那怎么选?最大生成树.可以保证选出的边限重尽可能大。
最大生成树的构建与最小生成树大同小异,我们只需将 Kruskal 算法中的排序变为从大到小排即可。
注意到 初始图不一定联通,那么我们会求出一个最大生成树森林。
#1.2 变树为链
现在,题目变成了:求从 \(x\) 到 \(y\) 的路径上的最小边权。
一看到树上的路径类问题,容易想到:树链剖分,但是,树剖太麻烦了,还不好调,我还懒得写,那怎么办?别急,我们慢慢来。
从 \(x\) 到 \(y\) 的路径就是从 \(x\) 到 \(\text{LCA}(x,y)\),再从 \(\text{LCA(x,y)}\) 到 \(y\),那我们就可以求从 \(x\) 到 \(\text{LCA}(x,y)\) 的最小边权,再求从 \(\text{LCA(x,y)}\) 到 \(y\) 的最小边权,比较两者的大小即可,那怎么求 \(\text{LCA}(x,y)\)?
答:树链剖分How old are you?(怎么老是你?),显然倍增啊!
所以,我们可以设 \(f_{i,k}\) 表示 \(i\) 的 \(2^k\) 辈祖先,\(g_{i,k}\) 表示从 \(i\) 到 \(i\) 的 \(2^k\) 辈祖先的路径上的最小边权,易知:
那么 \(f_{i,0}\) 便是 \(i\) 的父节点,\(g_{i,0}\) 便是从 \(i\) 到其父节点的边的权值。
这样的话,我们就可以用倍增预处理 \(f_{i,k}\) 和 \(g_{i,k}\),这里要注意我们之前求得的是最大生成树森林,可能有多于一颗的最大生成树。所以我们不能单单只从一个点开始。
#1.3 回答
至于回答时,可以直接采用倍增求 \(\text{LCA}\) 的框架,只不过在每次向上跳后更新下答案即可
#1.4 代码
const int N = 10010;
const int M = 50010;
const int INF = 0x3fffffff;
struct Edge{
int u,v;
int w,nxt;
};
Edge e[M << 2],es[M];
int tot,n,m,head[N],fa[N],vis[N],rt[N];
int f[N][50],t,d[N],g[N][50],qe;
queue <int> q;
inline int cmp(const Edge a,const Edge b){
return a.w > b.w;
}
inline void add(int u,int v,int w){
e[++ tot].u = u;
e[tot].v = v;
e[tot].w = w;
e[tot].nxt = head[u];
head[u] = tot;
}
inline int getf(int x){
while (x != fa[x])
x = fa[x] = fa[fa[x]];
return x;
}
inline void maxest(){ //求最大生成树
for (int i = 1;i <= n;i ++)
fa[i] = i;
sort(es + 1,es + m + 1,cmp);
for (int i = 1;i <= m;i ++){
int fx = getf(es[i].u);
int fy = getf(es[i].v);
if (fx == fy) continue;
add(es[i].u,es[i].v,es[i].w); //将最大生成树按邻接表存图
add(es[i].v,es[i].u,es[i].w);
fa[fx] = fy;
}
}
inline void prework(){ //倍增的预处理算法
tot = 0;mset(g,0x3f); //memset(g,0x3f,sizeof(g));
for (int i = 1;i <= n;i ++){ //这是一个最大生成树森林,所以不能只从一个点开始
if (vis[i]) continue;
rt[++ tot] = i;vis[i] = true;
q.push(i);
while (q.size()){
int now = q.front();q.pop();
for (int j = head[now];j;j = e[j].nxt){
int y = e[j].v;
if (vis[y]) continue;
f[y][0] = now;d[y] = d[now] + 1;
g[y][0] = e[j].w;vis[y] = true;
for (int k = 1;k <= t;k ++){
f[y][k] = f[f[y][k - 1]][k - 1];
g[y][k] = min(g[y][k - 1],g[f[y][k - 1]][k - 1]);
}
q.push(y);
}
}
}
}
inline int ExLCA(int x,int y){ //以倍增求 LCA 为框架
int res = INF;
if (d[x] < d[y]) swap(x,y);
for (int i = t;i >= 0;i --) //跳到同一高度
if (d[f[x][i]] >= d[y]){
res = min(res,g[x][i]);
x = f[x][i];
}
if (x == y) return res;
for (int i = t;i >= 0;i --) //同时向上跳
if (f[x][i] != f[y][i]){
res = min(res,g[y][i]);
y = f[y][i];
res = min(res,g[x][i]);
x = f[x][i];
}
res = min(res,g[x][0]); //更新答案
res = min(res,g[y][0]);
return res;
}
int main(){
scanf("%d%d",&n,&m);
t = (int)(log(n) / log(2)) + 1;
for (int i = 1;i <= m;i ++)
scanf("%d%d%d",&es[i].u,&es[i].v,&es[i].w);
maxest();prework();
scanf("%d",&qe);
while (qe --){
int x,y;
scanf("%d%d",&x,&y);
if (getf(x) != getf(y)){puts("-1");continue;} //判断是否联通
printf("%d\n",ExLCA(x,y));
}
return 0;
}
#2.0 P4116 Qtree3
#2.1 简单分析
熟悉的树上操作,又要见到我们的老朋友——树链剖分。
线段树中存储深度最浅的黑色节点的编号,正常的树剖即可。
#2.2 代码
const int N = 100010;
const int INF = 0x3fffffff;
struct Edge{
int u,v;
int nxt;
};
Edge e[N << 1];
struct Node{
int l,r;
int ls,rs;
int col;
int min_d,min_p;
//min_d 为胜读最浅的黑色节点的深度,其实可以不存,它相当于 d[min_p]
};
Node p[N << 2];
int n,q,tot,head[N],cnt;
int d[N],f[N],size[N],son[N];
int top[N],rk[N],id[N];
inline void add(int u,int v){
e[++ tot].u = u;
e[tot].v = v;
e[tot].nxt = head[u];
head[u] = tot;
}
inline void dfs1(int x,int fa,int depth){ //正常树剖即可
d[x] = depth;f[x] = fa;size[x] = 1;
for (int i = head[x];i;i = e[i].nxt){
if (e[i].v == fa) continue;
dfs1(e[i].v,x,depth + 1);
size[x] += size[e[i].v];
if (size[e[i].v] > size[son[x]])
son[x] = e[i].v;
}
}
inline void dfs2(int x,int t){
top[x] = t;id[x] = ++ tot;rk[tot] = x;
if (!son[x]) return;
dfs2(son[x],t);
for (int i = head[x];i;i = e[i].nxt){
if (e[i].v == f[x] || e[i].v == son[x])
continue;
dfs2(e[i].v,e[i].v);
}
}
inline void pushup(int k){ //更新节点信息
if (p[p[k].ls].min_d < p[p[k].rs].min_d){
p[k].min_d = p[p[k].ls].min_d;
p[k].min_p = p[p[k].ls].min_p;
}
else {
p[k].min_d = p[p[k].rs].min_d;
p[k].min_p = p[p[k].rs].min_p;
}
}
inline void build(int k,int l,int r){
if (l == r){
p[k].l = p[k].r = l;
p[k].min_d = INF;
p[k].min_p = 0;
p[k].col = 0;
return;
}
int mid = (l + r) >> 1;
p[k].ls = ++ cnt;
build(p[k].ls,l,mid);
p[k].rs = ++ cnt;
build(p[k].rs,mid + 1,r);
p[k].l = p[p[k].ls].l;
p[k].r = p[p[k].rs].r;
p[k].min_d = INF; //别忘了初始化QwQ
p[k].min_p = 0;
}
inline void change(int k,int x){ //单点修改
if (p[k].l == p[k].r){
p[k].col ^= 1;
if (p[k].col){ //要更改叶节点信息
p[k].min_d = d[rk[p[k].l]];
p[k].min_p = rk[p[k].l];
}
else {
p[k].min_p = 0;
p[k].min_d = INF;
}
return;
}
int mid = (p[k].l + p[k].r) >> 1;
if (x <= mid)
change(p[k].ls,x);
else
change(p[k].rs,x);
pushup(k);
}
inline int query(int k,int x,int y){
if (x <= p[k].l && p[k].r <= y)
return p[k].min_p;
int res = 0,mid = (p[k].l + p[k].r) >> 1;
if (x <= mid)
res = query(p[k].ls,x,y);
if (y > mid){
int gt = query(p[k].rs,x,y);
if (d[res] > d[gt])
res = gt;
}
return res;
}
inline int ask(int x){ //树剖求 LCA 的框架
int res = 0;
while (top[x] != 1){ //从 1 到 x 的路径
int gt = query(1,id[top[x]],id[x]);
if (d[res] > d[gt])
res = gt;
x = f[top[x]];
}
int gt = query(1,id[1],id[x]);
if (d[res] > d[gt])
res = gt;
return res;
}
int main(){
cin >> n >> q;
for (int i = 1;i < n;i ++){
int u,v;
cin >> u >> v;
add(u,v),add(v,u);
}
dfs1(1,0,1);
tot = 0;
dfs2(1,1);
build(++ cnt,1,n);
d[0] = INF;
while (q --){
int opt,b;
cin >> opt >> b;
if (!opt)
change(1,id[b]);
else{
int res = ask(b);
if (res) cout << res << endl;
else cout << -1 << endl;
}
}
return 0;
}
#3.0 P2801 教主的魔法
#3.1 简单分析
容易看出,这道题用线段树不好做,那就暴力分块.
分块后,对于每一个 a[i]
,都有一个替身使者 d[i]
与之对应,对于 每一个块内 的 d[i]
进行从小到大的排序,注意不是整体排序
修改的话,对于不是整块的点,要对 a[i]
进行更改,再将新的 a[i]
赋值给 d[i]
,再重新对块内的 d[i]
排序。对于整块的点,显然块中的相对大小没有改变,直接用 lazy[]
数组记录修改即可。
查询时,对于不是整块的点,朴素的扫一遍即可;但对于整块的,要在 d[i]
中二分查找第一个大于等于 \(x\) 的位置,用该块右端点编号减去查找到的位置编号再加一便是块中大于等于 \(x\) 的数的数量(这个活好像 lower_bound()
就能干)
#3.2 代码
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#define ll long long
#define mset(l,x) memset(l,x,sizeof(l))
using namespace std;
const int N = 1000010;
const int INF = 0x3fffffff;
int n,q,id[N],sub[N][2],len;
ll a[N],d[N],lazy[N];
inline int cmp(const ll &a,const ll &b){
return a < b;
}
inline void change(int l,int r,ll w){ //修改
int lp = id[l],rp = id[r];
if (lp == rp){
for (int i = l;i <= r;i ++)
a[i] += w;
for (int i = sub[lp][0];i <= sub[lp][1];i ++)
d[i] = a[i];
sort(d + sub[lp][0],d + sub[lp][1] + 1,cmp);
return;
}
for (int i = l;i <= sub[lp][1];i ++) //朴素修改
a[i] += w;
for (int i = sub[lp][0];i <= sub[lp][1];i ++)
d[i] = a[i];
sort(d + sub[lp][0],d + sub[lp][1] + 1,cmp);
for (int i = sub[rp][0];i <= r;i ++)
a[i] += w;
for (int i = sub[rp][0];i <= sub[rp][1];i ++)
d[i] = a[i];
sort(d + sub[rp][0],d + sub[rp][1] + 1,cmp);
for (int i = lp + 1;i < rp;i ++)
lazy[i] += w;
}
inline ll query(int l,int r,int x){ //查询
int lp = id[l],rp = id[r];
ll res = 0;
if (lp == rp){
for (int i = l;i <= r;i ++)
if (a[i] + lazy[lp] >= x) res ++;
return res;
}
for (int i = l;i <= sub[lp][1];i ++) //朴素统计
if (a[i] + lazy[lp] >= x) res ++;
for (int i = sub[rp][0];i <= r;i ++)
if (a[i] + lazy[rp] >= x) res ++;
for (int i = lp + 1;i < rp;i ++){
int ls = sub[i][0],rs = sub[i][1],ans = rs + 1;
while (ls <= rs){ //二分查找
int mid = (ls + rs) >> 1;
if (d[mid] + lazy[i] >= x)
ans = mid,rs = mid - 1;
else ls = mid + 1;
}
res += (sub[i][1] - ans + 1);
}
return res;
}
int main(){
scanf("%d%d",&n,&q);
len = sqrt(n);
for (int i = 1;i <= n;i ++){
scanf("%lld",&a[i]);
d[i] = a[i];
id[i] = (i - 1) / len + 1;
}
for (int i = 1;i < id[n];i ++){
sub[i][0] = (i - 1) * len + 1;
sub[i][1] = i * len;
sort(d + sub[i][0],d + sub[i][1] + 1,cmp);
}
sub[id[n]][0] = (id[n] - 1) * len + 1;
sub[id[n]][1] = n;
sort(d + sub[id[n]][0],d + sub[id[n]][1] + 1,cmp);
while (q --){
char opt;
int l,r,w;
cin >> opt >> l >> r >> w;
if (opt == 'M')
change(l,r,w);
else
printf("%lld\n",query(l,r,w));
}
return 0;
}