P4149 [IOI2011]Race
题目描述
给一棵树,每条边有权。求一条简单路径,权值和等于 \(k\) ,且边的数量最小。
输入格式
第一行包含两个整数 \(n,k\),表示树的大小与要求找到的路径的边权和。
接下来 \(n−1\) 行,每行三个整数 \(u_i\), \(v_i\) ,\(w_i\),代表有一条连接 \(u_i\) 与 \(v_i\),边权为 \(w_i\) 的无向边。
注意:点从 0 开始编号。
输出格式
输出一个整数,表示最小边数量。
如果不存在这样的路径,输出 −1。
输入输出样例
输入 #1
4 3
0 1 1
1 2 2
1 3 4
输出 #1
2
说明/提示
对于 100% 的数据,保证\(1\leq n\leq 2\times10^5\),\(1\leq k,w_i\leq 10^6\),\(0\leq u_i,v_i<n\)。
题解
庆祝一下,人生第一道 IOI 的题(之前写的题都太水了,不算)
第一个条件求树上长度为 \(k\) 的路径,一眼就能断定是点分治没跑了(好像学过点分治的都一眼秒了)。
第二个条件要求经过的边的数量最少,就记录一下每条路径经过的边的数量(感觉好水啊)。
之后暴力匹配长度为 \(k\) 的路径,看能否更新答案,如果经过的边的数量更少,就可以来更新答案。
其他的直接套点分治的模板就能 A 了此题啦。
至于怎么匹配,这里有两种写法:
- 双指针法(常数比较小的写法)
- 桶排序法(好写但注意的点比较多)
双指针法Code:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define int long long
const int N = 2e5+10;
int n,m,k,tot,cnt,root,sum_siz,u,v,w,ans = 2147483647;//ans 初值赋大一些
int head[N],siz[N],max_siz[N],dis[N];
bool vis[N];
struct bian
{
int to,net,w;
}e[N<<1];
struct node
{
int d,num,who;
node() {}
node(int a,int b,int c){num = a; d = b; who = c;}
}a[N<<1];
inline int read()
{
int s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s =s * 10+ch - '0'; ch = getchar();}
return s * w;
}
bool comp(node a,node b)//按边的长度排序,如果长度相同按经过的边的数量排序
{
if(a.d == b.d) return a.num < b.num;
return a.d < b.d;
}
void add(int x,int y,int w)
{
e[++tot].w = w;
e[tot].to = y;
e[tot].net = head[x];
head[x] = tot;
}
void get_root(int x,int fa)//找重心
{
siz[x] = 1; max_siz[x] = 0;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(to == fa || vis[to]) continue;
get_root(to,x);
siz[x] += siz[to];
max_siz[x] = max(max_siz[x],siz[to]);
}
max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}
void get_dis(int x,int fa,int num,int who)//找距离,num 记录经过的边的数量,who 记录他来自那颗子树
{
a[++cnt] = node(num,dis[x],who);//把路径信息存入结构体中
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to] || to == fa) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x,num+1,who);
}
}
int find(int x)//二分函数
{
int L = 1, R = cnt, ans = 0;
while(L <= R)
{
int mid = (L + R)>>1;
if(a[mid].d >= x)
{
ans = mid;
R = mid - 1;
}
else L = mid + 1;
}
return ans;
}
int calc(int x,int d)
{
cnt = 0; dis[x] = 0;
int res = 2147483647;
for(int i = head[x]; i; i = e[i].net)//找出所有子树中的路径
{
int to = e[i].to;
if(vis[to]) continue;
dis[to] = dis[x] + e[i].w;
get_dis(to,x,1,to);
}
a[++cnt] = node(0,0,0);
sort(a+1,a+cnt+1,comp);
int L = 1, R = cnt;//暴力双指针匹配
while(L <= cnt && a[L].d + a[R].d < k) L++;
while(L <= cnt)
{
if(k - a[L].d < a[L].d) break;
int tmp = find(k-a[L].d);
while(a[L].d + a[tmp].d == k && a[L].who == a[tmp].who) tmp++;//路径不能来自同一颗子树
if(a[L].d + a[tmp].d == k)
{
res = min(res,a[L].num+a[tmp].num);
}
L++;
}
return res;
}
void slove(int x)//点分治
{
ans = min(ans,calc(x,0));//统计一下这个点的答案·
vis[x] = 1;
for(int i = head[x]; i; i = e[i].net)
{
int to = e[i].to;
if(vis[to]) continue;//刚开始这里写挂了,嘤嘤嘤
max_siz[0] = n; sum_siz = siz[to]; root = 0;
get_root(to,0); slove(root);
}
}
signed main()
{
n = read(); k = read();
for(int i = 1; i <= n-1; i++)
{
u = read() + 1; v = read() + 1; w = read();
add(u,v,w); add(v,u,w);
}
max_siz[0] = sum_siz = n; root = 0;
get_root(1,0); slove(root);
if(ans > n) printf("%d\n",-1);//路径边数比 n 还大,直接判断无解
else printf("%lld\n",ans);
return 0;
}
我的桶排序的方法写挂了,就从可爱的 Tethys 那里扒了一份(感性理解一下吧)
Code:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5, INF = 1e9 + 5;
struct ed{
int u, v, nxt, w;
}edge[N << 1];
int n, m, cnt, k, a[N], head[N], siz[N], dis[N], mxsiz[N], root, tot, sum, ans, tt[1000006];
bool vis[N];//tt[i] 数组 表示长度为 i 的路径经过的边的数量最少是多少
void add(int u, int v, int val){
edge[++ cnt].u = u;
edge[cnt].v = v;
edge[cnt].w = val;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
void get_root(int now, int fa){
siz[now] = 1; mxsiz[now] = 0;
for(int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].v;
if(to == fa || vis[to]) continue;
get_root(to, now);
siz[now] += siz[to];
mxsiz[now] = max(mxsiz[now], siz[to]);
}
mxsiz[now] = max(mxsiz[now], sum - siz[now]);
if(mxsiz[now] < mxsiz[root]) root = now;
}
void get_dis(int now, int fa, int x, int y){
if(x > k) return ;//大于k的话直接返回,防止爆桶
dis[++ tot] = x;
a[tot] = y;
for(int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].v;
if(to == fa || vis[to]) continue;
get_dis(to, now, x + edge[i].w, y + 1);
}
}
void calc(int now){
tt[0] = tot = 0;
for(int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].v;
if(vis[to]) continue;
int l = tot;
get_dis(to, now, edge[i].w, 1);
for(int j = l + 1; j <= tot; j ++) ans = min(ans, tt[k - dis[j]] + a[j]); //先和别的子树中的边匹配来更新答案
for(int j = l + 1; j <= tot; j ++) tt[dis[j]] = min(tt[dis[j]], a[j]);//更新一下tt数组
}
for(int i = 1; i <= tot; i ++) tt[dis[i]] = INF;//每次计算完一个点的贡献都要把tt 数组赋为极大值
}
void solve(int now){
vis[now] = 1;
calc(now);
for(int i = head[now]; i; i = edge[i].nxt){
int to = edge[i].v;
if(vis[to]) continue;
mxsiz[0] = INF; sum = siz[to]; root = 0;
get_root(to, 0); solve(root);
}
}
int main(){
scanf("%d %d", &n, &k);
ans = INF;
for(int i = 1, x, y, z; i <= n - 1; i ++){
scanf("%d %d %d", &x, &y, &z);
add(x + 1, y + 1, z); add(y + 1, x + 1, z);
}
mxsiz[0] = INF; sum = n; root = 0;
memset(tt, 0x3f, sizeof(tt));
get_root(1, 0); solve(root);
if(ans > n) printf("-1\n");
else printf("%d\n", ans);
return 0;
}
Tethys 最可爱了
为什么 Tethys 的代码好短啊,自己拉行实锤了