2022杭电中超杯(1)个人题解


2022杭电中超杯(1)个人题解

1008 Path

算法知识点最短路分层图

题意

n n n个点 m m m条边 每条边有权值 w w w 。边的类型分为普通边和特殊边。对于特殊边来说 经过这条边后 可以使用魔法传送到任意点去,若传送的目标点与该点直接相连的,花费为 w i − k wi - k wik (保证不小于0),否则花费为 0 0 0

题目分析与思路

赛时没做出来,赛后看题解是分层图还用普通最短路wa了一发,先分析用普通最短路写的思路与错误,然后分析正解分层图。
看到题目并没有直接想到分层图而是对边分情况讨论直接跑最短路
1.对于普通边,直接跑最短路松弛。
2.对于特殊边,记录一下该点是否由特殊边而来,对于直接与该点相连的点 w = w − k w = w - k w=wk同样松弛即可。对于没有直接相连的点,全图大部分点都是我们的目标,我们可以STL set 维护所有点,使用花费为0的传送直接从 u u u到达 v v v 因为花费为 0 0 0 v v v是从当前与源点最近的点 u u u松弛而来,之后所有的点 d i s [ x ] dis[x] dis[x] 都不可能小于 d i s [ v ] = d i s [ u ] dis[v] = dis[u] dis[v]=dis[u] ,于是我们直接将点 v v v从set中删除即可,不需要重复遍历。

贴一下错误代码提供参考:不做注释,而之后的正解代码会有详细注释

#include <set>
#include <queue>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
const int N = 1e6 + 10;
const ll inf = 1e16;
int n,m,sta,k;
struct node
{
    int u,t;
    ll w;
    bool operator < (const node & A)const {
        return w > A.w;
    }
};
ll dis[N];
bool vis[N],st[N];
set<int>s;
vector<node>g[N];
priority_queue<node>q;
void djs()
{
	s.clear();
    q.push({sta , 0 , 0});
	for(int i=1;i<=n;i++)
	{
		dis[i] = inf;
		vis[i] = 0;
		if(i != sta) s.insert(i);
	}
    dis[sta] = 0;
    while(!q.empty())
    {
		node p = q.top();
		q.pop();
		int u = p.u , t = p.t;
		if(vis[u]) continue;
		vis[u] = 1;
		if(t == 1)
		{
			for(node e : g[u])
			{
				int v = e.u;
				st[v] = 1;
				ll w = max(1ll * 0 , e.w - k);
				if(dis[v] > dis[u] + w){
					dis[v] = dis[u] + w;
					q.push({v , 0 , dis[v]});
				}
			}
			vector <int> tmp;
			for(int v : s)
			{	
				if(!st[v] && dis[v] > dis[u]){
					tmp.push_back(v);
					dis[v] = dis[u];
					q.push({v , 0 , dis[v]});
				}
				st[v] = 0;
			}
			for(int v : tmp) s.erase(v);
		}
		else
		{
			for(node e : g[u])
			{
				int v = e.u;
				ll w = e.w;
				if(dis[v] > dis[u] + w){
					dis[v] = dis[u] + w;
					q.push({v , e.t , dis[v]});
				}
			}
		}
    }
}
void solve()
{
    scanf("%d%d%d%d",&n,&m,&sta,&k);
    for(int i=1;i<=n;i++)  g[i].clear(); 
    for(int i=1;i<=m;i++)
    {
        int u,v,w,t;
        scanf("%d%d%d%d",&u,&v,&w,&t);
        g[u].push_back({v,t,w});
    }
    djs();
    for(int i=1;i<=n;i++) {
        printf("%lld ",dis[i] < inf ? dis[i] : -1);
    }
	puts("");
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)solve();
    return 0;
}

这样的写法是可以过题目的样例的,莽一发后再次沉思为什么一定要用分层图,要用 d i s [ i ] [ 0 / 1 ] , v i s [ i ] [ 0 / 1 ] dis[i][0/1] ,vis[i][0/1] dis[i][0/1],vis[i][0/1]来代表从特殊边/普通边而来的距离和标记,直接分类讨论优先队列里记录从那种边而来不行吗。于是想到以下的问题:

dis[i][0] 代表普通边的图 1代表特殊边的图
为什么分层图? 若把经过普通边和特殊边的图放在一起存储 那么会发生如下的错误
数据:
4 3 1 1
1 2 3 1 1->2 花费3 且为特殊边
1 3 1 0 1->3 花费1 普通边
3 2 1 0 3->2 花费1 普通边
在这里插入图片描述

我们可以看出 若想到点4 必须经过1->2的特殊边后 经过传送到达4 d i s [ 4 ] = 3 dis[4] = 3 dis[4]=3

但实际上跑普通最短路不对图分层会出现bug 我们来模拟一下上面的数据:
优先队列中的变量 [ u , t y p e , d i s ] [u,type,dis] [u,type,dis] 分别代表点的编号,从哪一种而来,与源点的距离

1.通过边1->2松弛点2 d i s [ 2 ] = 3 dis[2] = 3 dis[2]=3 点(2,1,3)加入优先队列
2.通过边1->3松弛点3 d i s [ 3 ] = 1 dis[3] = 1 dis[3]=1 点(3,0,1)加入优先队列 点1的松弛结束
3.队列中点3的dis更小 将点3取出队列来松弛其他点 发现点2可以继续松弛 d i s [ 2 ] = d i s [ 3 ] + 1 = 2 dis[2] = dis[3] + 1 = 2 dis[2]=dis[3]+1=2将(2,0,2)加入优先队列
4.将点(2,0,2)取出队列发现没有能松弛的点
5.将点(2,1,3)取出队列
bug1 因为djs每次取出队列的元素会进行标记 点2已经在第4步被标记过了 vis[2] = 1 continue 点4未被遍历到 输出不能到达 显然错误
bug2 将代码改进一下 标记数组多一维对是否来自特殊边标记 v i s [ 2 ] [ 0 ] = 1 v i s [ 2 ] [ 1 ] = 0 vis[2][0] = 1 vis[2][1] = 0 vis[2][0]=1vis[2][1]=0 ,那么此时(2,1,3)可以继续松弛
但是松弛时我们用的dis不是从队列里取出来的那一个 而是dis数组中的被松弛过的dis[2] 那么dis[4] = dis[2] = 2 显然也是错误
那么只能dis数组也多开一维记录了,这个做法就是分层图的思想

AC代码

#include <set>
#include <queue>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
const int N = 1e6 + 10;
const ll inf = 1e16;
int n,m,sta,k;
struct node
{
    int u,ty;
    ll w;
    bool operator < (const node & A)const {
        return w > A.w;
    }
};
int st[N];//对当前点的邻接点染色
ll dis[N][2];
bool vis[N][2];//标记

set<int>s;//存的是图中所有点集
vector<node>g[N];
priority_queue<node>q;
void djs()
{
    q.push({sta , 0 , 0});
    for(int i=1;i<=n;i++)
    {
        dis[i][0] = dis[i][1] = inf;   
        vis[i][0] = vis[i][1] = 0;
        if(i != sta) s.insert(i); //起点已经是最短的当然不用入队
    }
    dis[sta][0] = 0;
    int color = 0;
    while(!q.empty())
    {
        node p = q.top(); q.pop();
        int u = p.u , ty = p.ty;
        if(vis[u][ty]) continue ;
        vis[u][ty] = 1;
        s.erase(u);
        color ++; // 对该点的邻接点染色 的颜色
        if(ty == 1)
        {
            for(node e : g[u]) st[e.u] = color;
            vector <int> tmp ;
            for(int v : s){
                if(st[v] != color) tmp.push_back(v); //将非邻接点存入临时vec
            }
            for(int v : tmp){//tmp中全是非邻接点可以花费代价为0进行传送
                s.erase(v); // dis[v] = dis[u] 已经等于离起点最接近的点直接erase
                dis[v][0] = dis[u][ty];
                q.push({v , 0 , dis[v][0]});
            }
        } 
        ll base = ty == 0 ? 0 : k;// 是特殊边可以减免花费 
        for(node e : g[u])
        {
            int v = e.u;
            ll w = e.w - base;
            if(dis[v][e.ty] > dis[u][ty] + w){
                dis[v][e.ty] = dis[u][ty] + w;
                q.push({v , e.ty , dis[v][e.ty]});//就算是传送到达的点v 如果ty = 1 也被认为是经过了特殊边
            }
        }
    }
}
void solve()
{
    scanf("%d%d%d%d",&n,&m,&sta,&k);
    for(int i=1;i<=n;i++){
        g[i].clear();  
        st[i] = 0;
    }
    for(int i=1;i<=m;i++)
    {
        int u,v,w,t;
        scanf("%d%d%d%d",&u,&v,&w,&t);
        g[u].push_back({v,t,w});
    }
    djs();
    for(int i=1;i<=n;i++) {
        ll dist = min(dis[i][0] , dis[i][1]);
        printf("%lld ",dist < inf ? dist : -1);
    }
    puts("");
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)solve();
    return 0;
}

其他题目也会陆续更新。

更新:
7.27 1008 path

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值