T1 shinyruo溜冰(课堂练习)
题目大意:
给定 n n n个点的坐标 ( x i , y i ) (x_i,y_i) (xi,yi),从一个点 i i i转移到另一个点 j j j的代价是 m i n { a b s ( x i − x j ) , a b s ( y i − y j ) } min \lbrace abs(x_i-x_j),abs(y_i-y_j)\rbrace min{abs(xi−xj),abs(yi−yj)},记转移到 x , y x,y x,y的最小代价为 c o s t ( x , y ) cost(x,y) cost(x,y),现试求从 1 1 1转移到 n n n的最小代价 c o s t ( x n , y n ) cost(x_n,y_n) cost(xn,yn)。
题目解读与分析
先直接上题解思路:
考虑到两个点间如果要走
x
x
x方向,那么通过
x
x
x方向上的中间节点走过去代价只少不多;
y
y
y方向同样如此。
符号语言就是:
∀
x
i
<
x
k
<
x
j
,
c
o
s
t
(
i
,
j
)
≥
c
o
s
t
(
i
+
k
)
+
c
o
s
t
(
k
+
j
)
,
y
方向同理
\forall x_i<x_k<x_j,cost(i,j)\ge cost(i+k)+cost(k+j),y方向同理
∀xi<xk<xj,cost(i,j)≥cost(i+k)+cost(k+j),y方向同理
也就是说,当我们分两次将所有点按
x
x
x坐标和
y
y
y坐标排序后,对x坐标相邻的两个节点和
y
y
y坐标相邻的两个节点进行连边,这样就构成了唯一的两种转移方法,因为两点之间连点不会跳跃中间的点,所以我愿称之为:“不跳跃连边”。并且这两种方向上的转移方法最后因为也要求最短路所以符合题目中两者取其小的要求。同时连边的复杂度被划分为了
O
(
n
)
O(n)
O(n)
本人思路:
琢磨了半天琢磨出了两点:
@1 两个点中间(
x
x
x和
y
y
y都满足在两点的内部)的点如果存在,走这个点更好。
@2 无论怎样,最优的转移方式一定是通过一次
x
x
x转移然后通过一次
y
y
y转移(或者两者代价相同时无差异)
第一点不用说,就是正解思路的弱化版,如果用其来做的话是
O
(
n
2
)
O(n^2)
O(n2)连边,所以有问题。
想到第二个点我就死盯着这个最优转移,想了无数遍爆搜,就是没有想过最短路(当然也是因为@1是
O
(
n
2
)
O(n^2)
O(n2)所以无法用最短路做)。不过我认为第二点还是挺特殊的,只是目前我还不知道如何使用这一点。
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n,fs[N],to[N<<2],nex[N<<2],w[N<<2],cnt;//要连4 * N条边
int Read()
{
int x = 0,k = 1;char ch = getchar();
while(!isdigit(ch)) {if(ch == '-') k = -1;ch = getchar();}
while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - '0';ch = getchar();}
return x * k;
}
struct node{
int x,y,st;
}pos[N];
node p2[N];
void add(int x,int y,int z)
{nex[++cnt]=fs[x],fs[x]=cnt,to[cnt]=y,w[cnt]=z;nex[++cnt]=fs[y],fs[y]=cnt,to[cnt]=x,w[cnt]=z;}
bool comp1(node A,node B)//两种排序
{
return A.x < B.x;
}
bool comp2(node A,node B)
{
return A.y < B.y;
}
priority_queue<pair<int,int> >q;
int dis[N],vis[N];
void dj()
{
dis[1] = 0,q.push(make_pair(0,1));
while(q.size())
{
int u = q.top().second;q.pop();
if(vis[u]) continue;vis[u] = 1;
for(int e = fs[u];e;e = nex[e])
{
int v = to[e];
if(dis[v] > dis[u] + w[e])
{
dis[v] = dis[u] + w[e];
q.push(make_pair(-dis[v],v));
}
}
}
}
int main()
{
n = Read();
memset(dis,0x3f3f3f,sizeof dis);
for(int i = 1;i <= n; i++)
{
p2[i].x = Read(),p2[i].y = Read();
p2[i].st = i;
}
sort(p2 + 1,p2 + n + 1,comp1);//按x排序
for(int i = 1;i < n; i++) add(p2[i].st,p2[i+1].st,-p2[i].x + p2[i + 1].x);//x方向直接连
sort(p2 + 1,p2 + n + 1,comp2);//按y排序
for(int i = 1;i < n; i++) add(p2[i].st,p2[i+1].st,-p2[i].y + p2[i + 1].y);//y方向直接连
dj();
cout << dis[n];return 0;
}
分层图
我们在看T2前先来了解一下分层图
例题飞行路线(洛谷例题)
题目描述
Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司。该航空公司一共在 n n n 个城市设有业务,设这些城市分别标记为 0 0 0 到 n − 1 n-1 n−1,一共有 m m m 种航线,每种航线连接两个城市,并且航线有一定的价格。
Alice 和 Bob 现在要从一个城市沿着航线到达另一个城市,途中可以进行转机。航空公司对他们这次旅行也推出优惠,他们可以免费在最多 k k k 种航线上搭乘飞机。那么 Alice 和 Bob 这次出行最少花费多少?
输入格式
第一行三个整数 n , m , k n,m,k n,m,k,分别表示城市数,航线数和免费乘坐次数。
接下来一行两个整数 s , t s,t s,t,分别表示他们出行的起点城市编号和终点城市编号。
接下来 m m m 行,每行三个整数 a , b , c a,b,c a,b,c,表示存在一种航线,能从城市 a a a 到达城市 b b b,或从城市 b b b 到达城市 a a a,价格为 c c c。
输出格式
输出一行一个整数,为最少花费。
数据规模与约定
对于 30 % 30\% 30% 的数据, 2 ≤ n ≤ 50 2 \le n \le 50 2≤n≤50, 1 ≤ m ≤ 300 1 \le m \le 300 1≤m≤300, k = 0 k=0 k=0。
对于 50 % 50\% 50% 的数据, 2 ≤ n ≤ 600 2 \le n \le 600 2≤n≤600, 1 ≤ m ≤ 6 × 1 0 3 1 \le m \le 6\times10^3 1≤m≤6×103, 0 ≤ k ≤ 1 0 \le k \le 1 0≤k≤1。
对于 100 % 100\% 100% 的数据, 2 ≤ n ≤ 1 0 4 2 \le n \le 10^4 2≤n≤104, 1 ≤ m ≤ 5 × 1 0 4 1 \le m \le 5\times 10^4 1≤m≤5×104, 0 ≤ k ≤ 10 0 \le k \le 10 0≤k≤10, 0 ≤ s , t , a , b ≤ n 0\le s,t,a,b\le n 0≤s,t,a,b≤n, a ≠ b a\ne b a=b, 0 ≤ c ≤ 1 0 3 0\le c\le 10^3 0≤c≤103。
题目分析与解读
我们发现,该题就是最短路模板加上一个可以免费选边经过的题目,接下来我们就要考虑如何处理这个免费选边。
发现就可以将可以免费选择的边的数量视为一种状态,我们又知道在最短路中,所在点也是一种状态,那么这道题的本质就是两种状态的共同转移与相互转化,这也启发了我们的思维:
很多具有可转移性的限制其实也是一种状态,比如这里的免费选边个数,相对地,像地图的边界或者障碍就是不可转移的。
所以我们就容易想到存储每个点和其剩余的选边数的状态的dp;
即
d
i
s
[
u
]
[
j
]
dis[u][j]
dis[u][j]表示u节点剩余j次免费跳边的最小距离。
然后有转移式:
u
—
e
—
>
v
u—e—>v
u—e—>v
d
i
s
[
v
]
[
j
]
=
m
i
n
(
d
i
s
[
u
]
[
j
+
1
]
,
d
i
s
[
u
]
[
j
]
+
w
[
e
]
)
dis[v][j] = min(dis[u][j + 1],dis[u][j] + w[e])
dis[v][j]=min(dis[u][j+1],dis[u][j]+w[e])
那么上述所讲与分层图有什么关系呢?其实,分层图就是dp思想的形象转化。他将所剩的选择免费边数直接转化为图的信息:即k层不同的图,处于第j层的还有k - j + 1层免费跳边,再加上从较低层到较高层的单向连边,便真正意义上的模拟出了我们需要处理的免费边数的状态。
温馨提醒
最后最短距离不一定是用了k次免费边到达终点的距离,比如k = 2,st = 1,ed = 2: 1—3—>2 3—2—>2 dis[2][1] = 0,dis[2][0] = 2因为第一次免费走到了2之后还往外走到了3再免费走回来,反而多花费2的代价。
这里附上更容易理解的分层图代码:
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;//point will repeat for k times
const int M=3e6+10;
int n,m,k,st,ed;
int dis[N],fs[N],nex[M],to[M],w[M],tot=2,vis[N];
struct node{
int dis,id;
bool operator < (const node &a) const{
return dis > a.dis;
}
};
priority_queue<node> q;
void add(int x,int y,int z=0)
{
nex[++tot]=fs[x];
fs[x]=tot;
to[tot]=y;
w[tot]=z;
}
void dj()
{
memset(dis,0x3f,sizeof(dis));
dis[st]=0;
q.push(node{0,st});
while(!q.empty())
{
int u=q.top().id;
q.pop();
if(vis[u]) continue;
vis[u]=1;
for(int e=fs[u];e;e=nex[e])
{
int v=to[e];
if(dis[u]+w[e]<dis[v])
{
dis[v]=dis[u]+w[e];
q.push(node{dis[v],v});
}
}
}
}
int Read()
{
int x=0,k=1;char ch=getchar();
while(ch-'0'<0 or ch-'0'>9) {if(ch=='-') k=-1;ch=getchar();}
while(ch-'0'>=0 and ch-'0'<=9) {x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x*k;
}
int main()
{
int v,u,c;
scanf("%d%d%d%d%d",&n,&m,&k,&st,&ed);
for(int i=0;i<m;++i)
{
scanf("%d%d%d",&u,&v,&c);
add(u,v,c);
add(v,u,c);
for(int j=1;j<=k;++j)
{
add(u+(j-1)*n,v+j*n);//默认加入边权为0的边,从第j层到j + 1层
add(v+(j-1)*n,u+j*n);
add(u+j*n,v+j*n,c);
add(v+j*n,u+j*n,c);
}
}
dj();
int ans = INT_MAX;
for(int i = 0;i <= k; i++)ans = min(ans,dis[ed + i * n]);
printf("%d",ans);
return 0;
}
T2 最短路
题目大意:
一个路径
S
=
S=
S={
e
1
,
e
2
.
.
.
e_1,e_2...
e1,e2...}的权值表示为:
w
S
=
∑
e
∈
S
w
e
−
m
a
x
e
∈
S
w
e
+
m
i
n
e
∈
S
w
e
w_S = \sum_{e\in S}w_e - max_{e\in S}w_e + min_{e\in S}w_e
wS=∑e∈Swe−maxe∈Swe+mine∈Swe
现试求节点1到2 - n点的最小权值路径的权值。
题目解读与分析
我们尝试通过特殊处理将两个特殊的权值计算也转换为图的元素,可以理解为:+max就是使为max的边权值变为两倍,-min就是使为min的边权值变为0,然后发现这样建边后所走的最短路恰好就满足路径贡献最小,这便是这道题的核心。
但是走双倍权值和零权值边每次只能走一遍,所以我们需要进行设置:
先走0再走双倍的状态
先走双倍再走0的状态
dp状态也就出来了。
转化为分层图就是:
第一层是原层,第二层是先走
0
0
0 边权,第三层是先走2倍边权,第四层是汇合点。
温馨提示
此处本来只能使用第四层的距离,因为是硬性要求要跳两次层的,而不是像例一飞行路线一样可以不把免费边用完。但是但是,有些点的最小值是由1节点走一步到达形成的,此时如果分层图上既走0边权又走2倍边权要走两步,是无法实现的,所以对全体点取一个 m i n ( d i s [ i ] , d i s [ i + 3 ∗ n ] ) min(dis[i],dis[i + 3 * n]) min(dis[i],dis[i+3∗n]),即第一层也要考虑
参考代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 6e6 + 10;
int n,m;
int Read()
{
int x = 0,k = 1;char ch = getchar();
while(!isdigit(ch)) {if(ch == '-') k = -1;ch = getchar();}
while(isdigit(ch)) {x = (x << 3) + (x << 1) + ch - '0';ch = getchar();}
return x * k;
}
int fs[N],nex[N<<1],to[N<<1],cnt;
ll w[N<<1];
void add(int x,int y,ll z) {nex[++cnt]=fs[x];fs[x]=cnt;to[cnt]=y;w[cnt]=z;}
void Add(int x,int y,ll z) {add(x,y,z),add(y,x,z);}
ll dis[N];
int vis[N];
struct node {
long long dis;
int id;
bool operator < (node A) const {
return dis>A.dis;
}
};//以后还是写结构体吧,我这道题用小根堆加负dis会出错,又不想打大根堆
priority_queue<node> q;
void dj()
{
q.push(node{0,1});
while(q.size())
{
int u = q.top().id;q.pop();
if(vis[u]) continue;vis[u] = 1;
for(int e = fs[u];e;e = nex[e])
{
int v = to[e];
if(dis[v] > dis[u] + w[e]) {dis[v] = dis[u] + w[e],q.push(node{(dis[v] - 1000),v});}
}
}
}
signed main()
{
n = Read(),m = Read();int x,y;
ll z;
for(int i = 2;i <= 4 * n; i++) vis[i] = 0,dis[i] = 1e18;
for(int i = 1;i <= m; i++)
{
x = Read(),y = Read(),z = 1ll * Read();
Add(x,y,z),Add(x+n,y+n,z),Add(x+2*n,y+2*n,z),Add(x + 3 * n,y + 3 * n,z);//每层之间互相连边
add(x,y+n,0),add(y,x + n,0);//先走第二层0边权
add(x + n,y + 3 * n,2*z),add(y + n,x + 3 * n,2 * z);//从第二层到第四层2倍边权
add(x ,y + 2 * n,z<<1),add(y ,x + 2 * n,z<<1);//先走第三层2倍边权
add(x + 2 * n,y + 3 * n,0),add(y + 2 * n,x + 3 * n,0);//从第三层到第四层2倍边权
}
dis[1] = 0;
dj();
for(int i = 2;i <= n; i++)
printf("%lld ",min(dis[i],dis[i + 3 * n]));//只能求第四层或第一层的值
}
小结
在面对图的特殊状态转移时要多想一想这些特殊的建边,同时可以根据时间复杂度或空间复杂度来辅助思考。