[USACO 2021]Tickets题解
[USACO 2021 December, Platinum] [luoguP7984] Problem 1. Tickets
Bessie 正在参加远足旅行!她当前正在旅行的路线由编号为 \(1…N\)(\(1≤N≤10^5\))的 \(N\) 个检查点组成。
有 \(K\)(\(1≤K≤10^5\))张票可供购买。第 ii 张票可以在检查站 \(c_i\)(\(1≤c_i≤N\))以 \(p_i\)(\(1≤p_i≤10^9\))的价格购得,并且可以用其进入所有检查站 \([a_i,b_i]\)(\(1≤a_i≤b_i≤N\))。在进入任何检查站之前,Bessie 必须已购买一张允许其进入该检查站的票。一旦 Bessie 可以前往某一检查站,她就可以在未来的任何时候回到该检查站。
对于每一个$ i∈[1,N]$,如果 Bessie 最初只能进入检查点 \(i\),输出使得可以进入检查点 \(1\) 和$ N$ 所需的最低总价。如果无法这样做,输出 \(−1\)。
输入格式(从终端 / 标准输入读入):
输入的第一行包含 \(N\) 和$ K$。
以下 \(K\) 行,对于每一个 \(1≤i≤K\),第 \(i\)行包含四个整数 \(c_i\),\(p_i\),\(a_i\)和 \(b_i\)。
输出格式(输出至终端 / 标准输出):
输出$ N$ 行,每行输出一个检查点的答案。
输入样例:
7 6
4 1 2 3
4 10 5 6
2 100 7 7
6 1000 1 1
5 10000 1 4
6 100000 5 6
输出样例:
-1
-1
-1
1111
10100
110100
-1
参考图:

反图

如果 Bessie 从检查点 \(i=4\) 开始,那么一种购得进入检查点 \(1\) 和 \(N\)的方法如下:
- 在检查点 \(4\) 购买第一张票,使 Bessie 可以进入检查点 \(2\) 和 \(3\)
- 在检查点 \(2\) 购买第三张票,使 Bessie 可以进入检查点 \(7\)。
- 回到检查点 \(4\) 购买第二张票,使 Bessie 可以进入检查点 \(5\) 和 \(6\)。
- 在检查点 \(6\) 购买第四张票,使 Bessie 可以进入检查点 \(1\)。
测试点性质:
- 测试点 1-7 满足 \(N,K≤1000\)。
- 测试点 8-19 没有额外限制。
别的大神写的详细解释(太强了,%%%)
以下是我写的:
这是USACO 2021年12月铂金组第一题《Tickets》的题解代码,核心思路是线段树优化建图+三次Dijkstra算法,解决了大规模节点下的最短路径问题。
一、问题核心分析Bessie需要从每个检查点i出发,购买门票获得进入1号和N号检查点的权限,求最低总花费,无法完成则输出-1。门票的购买规则为:在指定检查点\(c_i\)花费\(p_i\)购买后,可进入\([a_i, b_i]\)区间内的所有检查点,且获得权限后可随时返回已访问的检查点。
二、核心解题思路
- 线段树优化建图:如果直接将门票对应的区间\([a_i, b_i]\)的每个点都和门票节点连边,总边数会达到\(O(K*N)\),无法通过大规模测试点。因此使用线段树将区间连边优化到\(O(K \log N)\)的级别,大幅降低边数。
- 三次Dijkstra算法
- 第一次以1号检查点为起点,计算所有节点到1号点的最短路径\(d_1\);
- 第二次以N号检查点为起点,计算所有节点到N号点的最短路径\(d_n\);
- 第三次将所有节点的\(d_1+d_n\)作为初始距离,运行多源Dijkstra,最终得到每个原始检查点到同时访问1号和N号点的最低花费。
三、代码逐行详解
const int N=2e5+5;
const int inf=1e18;
int n,k,c[N],p[N],a[N],b[N],tot,d1[10*N],dn[10*N],dis[10*N];
vector> ee[10*N];
struct Tr{int l,r,ls,rs;}tr[4*N];
- 定义常量:N为最大检查点数量,inf为无穷大表示不可达;
- 全局变量:n、k为输入的检查点和门票数量,c/p/a/b数组存储每张门票的信息,tot为总节点数(初始为n,后续添加线段树节点和门票节点);
- ee为邻接表存储图的边,tr为线段树节点结构体。
int build(int l,int r)
{
int id=++tot;
tr[id].l=l;
tr[id].r=r;
if(l==r)
{
tr[id].ls=tr[id].rs=0;
ee[id].push_back({l,0});
ee[l].push_back({id,0});
return id;
}
int mid=(l+r)/2;
tr[id].ls=build(l,mid);
tr[id].rs=build(mid+1,r);
ee[tr[id].ls].push_back({id,0});
ee[tr[id].rs].push_back({id,0});
return id;
}
- 线段树构建函数:递归构建覆盖1~n区间的线段树;
- 叶子节点和对应原始检查点互相连边,边权为0(表示可以自由转换);
- 非叶子节点和左右子节点连边,边权为0(表示可以在区间内自由移动)。
void query(int id,int l,int r,int u)
{
if(tr[id].r<l||tr[id].l>r) return;
if(l<=tr[id].l&&tr[id].r<=r)
{
ee[id].push_back({u,0});
return;
}
query(tr[id].ls,l,r,u);
query(tr[id].rs,l,r,u);
}
- 线段树查询函数:查询\([a_i, b_i]\)区间,将所有覆盖该区间的线段树节点和对应门票节点u连边,边权为0,表示持有该门票后可以访问区间内所有检查点。
void dij(int s,int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
fill(d,d+10*N,inf);
d[s]=0;
q.push({0,s});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:ee[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
- 单源Dijkstra算法:以s为起点,计算所有节点的最短路径,结果存储在d数组中。
void dij1(int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
for(int i=1;i<=tot;i++) if(d[i]<inf) q.push({d[i],i});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:ee[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
- 多源Dijkstra算法:将所有初始距离小于inf的节点都加入优先队列,同时计算所有节点的最短路径,对应第三次Dijkstra的步骤。
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=k;i++) cin>>c[i]>>p[i]>>a[i]>>b[i];
tot=n;
int root=build(1,n);
for(int i=1;i<=k;i++)
{
int id=++tot;
ee[id].push_back({c[i],p[i]});
query(root,a[i],b[i],id);
}
dij(1,d1);
dij(n,dn);
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=tot;i++) dis[i]=d1[i]+dn[i];
dij1(dis);
for(int i=1;i<=n;i++)
{
if(dis[i]>=inf) cout<<"-1\n";
else cout<<dis[i]<<"\n";
}
return 0;
}
主函数流程:
- 输入n和k,以及k张门票的信息;
- 初始化总节点数为n,构建线段树;
- 为每张门票创建节点,在购买点\(c[i]\)和门票节点之间连边,边权为\(p[i]\)(购买门票的花费),同时将门票节点和其覆盖的\([a_i, b_i]\)区间连边;
- 运行两次单源Dijkstra,分别得到所有节点到1号点和N号点的最短路径;`
- 计算每个节点的\(d1+dn\)作为初始距离,运行多源Dijkstra,得到每个节点到同时访问1号和N号点的最低花费;`
- 输出每个原始检查点的结果,不可达则输出-1。
四、复杂度分析
- 建图阶段:构建线段树的时间复杂度为\(O(N)\),处理K张门票的时间复杂度为\(O(K \log N)\),总边数为\(O(N + K \log N)\);
- 三次Dijkstra的时间复杂度为\(O((N + K\log N) \log(N + K \log N))\),整体复杂度为\(O((N + K \log N) \log N)\),可以通过N、K为\(10^5\)的测试点。
完整代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int inf=1e18;
int n,k,c[N],p[N],a[N],b[N],tot,d1[10*N],dn[10*N],dis[10*N];
vector<pair<int,int>> ee[10*N];
struct Tr{int l,r,ls,rs;}tr[4*N];
int build(int l,int r)
{
int id=++tot;
tr[id].l=l;
tr[id].r=r;
if(l==r)
{
tr[id].ls=tr[id].rs=0;
ee[id].push_back({l,0});
ee[l].push_back({id,0});
return id;
}
int mid=(l+r)/2;
tr[id].ls=build(l,mid);
tr[id].rs=build(mid+1,r);
ee[tr[id].ls].push_back({id,0});
ee[tr[id].rs].push_back({id,0});
return id;
}
void query(int id,int l,int r,int u)
{
if(tr[id].r<l||tr[id].l>r) return;
if(l<=tr[id].l&&tr[id].r<=r)
{
ee[id].push_back({u,0});
return;
}
query(tr[id].ls,l,r,u);
query(tr[id].rs,l,r,u);
}
void dij(int s,int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
fill(d,d+10*N,inf);
d[s]=0;
q.push({0,s});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:ee[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
void dij1(int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
for(int i=1;i<=tot;i++) if(d[i]<inf) q.push({d[i],i});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:ee[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=k;i++) cin>>c[i]>>p[i]>>a[i]>>b[i];
tot=n;
int root=build(1,n);
for(int i=1;i<=k;i++)
{
int id=++tot;
ee[id].push_back({c[i],p[i]});
query(root,a[i],b[i],id);
}
dij(1,d1);
dij(n,dn);
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=tot;i++) dis[i]=d1[i]+dn[i];
dij1(dis);
for(int i=1;i<=n;i++)
{
if(dis[i]>=inf) cout<<"-1\n";
else cout<<dis[i]<<"\n";
}
return 0;
}
注意:
别的大神写的详细解释,关于第三次dij可能有问题,那上面说第三次dij要用原图,但我用原图在我们OJ上错了(也许是因为OJ上的数据更水?逃)
第三次dij用原图的代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int inf=1e18;
int n,k,c[N],p[N],a[N],b[N],tot,d1[10*N],dn[10*N],dis[10*N];
vector<pair<int,int>>e[10*N],ee[10*N];
struct Tr{int l,r,ls,rs;}tr[4*N];
int build(int l,int r)
{
int id=++tot;
tr[id].l=l;
tr[id].r=r;
if(l==r)
{
tr[id].ls=tr[id].rs=0;
e[id].push_back({l,0});
e[l].push_back({id,0});
ee[id].push_back({l,0});
ee[l].push_back({id,0});
return id;
}
int mid=(l+r)/2;
tr[id].ls=build(l,mid);
tr[id].rs=build(mid+1,r);
e[id].push_back({tr[id].ls,0});
e[id].push_back({tr[id].rs,0});
ee[tr[id].ls].push_back({id,0});
ee[tr[id].rs].push_back({id,0});
return id;
}
void query(int id,int l,int r,int u)
{
if(tr[id].r<l||tr[id].l>r) return;
if(l<=tr[id].l&&tr[id].r<=r)
{
e[u].push_back({id,0});
ee[id].push_back({u,0});
return;
}
query(tr[id].ls,l,r,u);
query(tr[id].rs,l,r,u);
}
void dij(int s,int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
fill(d,d+10*N,inf);
d[s]=0;
q.push({0,s});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:ee[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
void dij1(int d[])
{
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>>q;
for(int i=1;i<=tot;i++) if(d[i]<inf) q.push({d[i],i});
while(!q.empty())
{
int dist=q.top().fi;
int u=q.top().se;
q.pop();
if(dist>d[u]) continue;
for(auto i:e[u])
{
int v=i.fi,w=i.se;
if(d[v]>d[u]+w)
{
d[v]=d[u]+w;
q.push({d[v],v});
}
}
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>k;
for(int i=1;i<=k;i++) cin>>c[i]>>p[i]>>a[i]>>b[i];
tot=n;
int root=build(1,n);
for(int i=1;i<=k;i++)
{
int id=++tot;
e[c[i]].push_back({id,p[i]});
ee[id].push_back({c[i],p[i]});
query(root,a[i],b[i],id);
}
dij(1,d1);
dij(n,dn);
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=tot;i++) dis[i]=d1[i]+dn[i];
dij1(dis);
for(int i=1;i<=n;i++)
{
if(dis[i]>=inf) cout<<"-1\n";
else cout<<dis[i]<<"\n";
}
return 0;
}
OJ记录:2RE+8WA
用反图才AC,AI说:

班里有人对第三次dij的正确性提出疑问,以下是某大神的证明,证明如下:

设\(d_{i,l}\)表示从\(i\)到\(j\)需要的最小代价,\(dis_i=d_{1,i}+d_{i,n}\),由图可得,对点\(j\)做松弛操作为,\(dis_j=\min\){\(dis_j,dis_i+w\)}
-
\(dis_i\)的路径不经过\(j\)(\(1\rightarrow i,i\rightarrow n\)),直接维护就行,不存在多加
-
\(dis_i\)的路径经过\(j\)(\(1\rightarrow i,i\rightarrow j,j\rightarrow n\)),
则\(dis_i=d_{1,i}+w+d_{j,n}\),代入得\(dis_j=\min\){\(dis_j,d_{1,i}+2*w+d_{j,n}\)},
\(dis_j=d_{1,j}+d_{n,j}\),对\(d_{1,j}\)拆贡献,可得\(d_{1,j}=\min\){\(D,d_{1,i}+w\)}
-
\(D<d_{1,i}+w\),
则\(d_{1,j}=D\),\(dis_j=D+d_{j,n}\)
因为\(D<d_{1,i}+w\),所以\(D<d_{1,i}+2*w\),所以\(D+d_{j,n}<d_{1,i}+2*w+d_{j,n}\),不会多加
-
\(D<d_{1,j}+w\)
则\(d_{1,j}=d_{1,i}+w\),\(dis_j=d_{1,i}+w+d_{i,n}<d_{1,i}+2*w+d_{j,n}\),只会加一个\(w\),
-
综上,第三次dij时不会多加w
其实我没看懂,看懂的请评论告诉我
欢迎各位大佬出数据反驳我
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23075810

浙公网安备 33010602011771号