图论专题 - 解题报告 - E

本文分享了一次尝试解决图论问题的经历,最初使用BFS求最短路导致TLE,后了解到正解是通过“拆点”方法。拆点将边转化为两个节点间的路径,简化了问题并确保符合最短路规则。最终通过实现拆点后的最短路算法成功解决问题。
摘要由CSDN通过智能技术生成

这题是我最先开始做的一批题之一,糖哥tql,您带的榜可卡死我咯。一开始想到一个很单纯的套路,就是搜,最短路,最少代价最先到达,BFS走起冲冲冲,每次到下一个层次的新点就延申出一条边,一次性更新全部同色的边上点的深度。每次广搜扩展就有了很多跳过条件,能保证每条边最多遍历一次,这么听起来好像算是O(e)的算法?但是由于每次在一个点上判断次数过多,所以这么做基本上是必t的,加了算是优秀的剪枝也过不了19点。糖哥先过的E嘛,我问他前面的点跑的多快,发现自己前面的速度快到起飞,正解13点150ms我能跑出35ms,所以甚至有了这个爆搜有可能的念头,糖哥还陪着我一起调了好久hhhh。(在最后这几天糖哥把这个方法调教出来了,用了multiset去重和排序etc,当然最后不能算我的东西代码我就不贴了,但是也学到了点东西)
虽然一直在TLE的样子很狼狈,但是不懈调参爆破的样子真的很靓仔
贴一下自己的一个版本,纪念一下:

//这个版本不是最终的版本没有加删边的优化过不了18点,但是比我瞎删边的那个版本思路清晰很多
void update(int now, int d, int c, int father)  //同色更新
{
    if (now == n)
    {
        printf("%d\n", d - 1);
        exit(0);
    }
    for (int i = head[now]; i; i = edge[i].nex)
    {
        int to = edge[i].to, color = edge[i].col;						//颜色是边上的性质
        if (father == to) continue;
        if (color != c) continue;       //同色边即可不花费深度更新
        if (depth[to] < d) continue;
        if (edge[i].vis) continue;
        if (depth[to] != d)
        {
            depth[to] = d;
            v[d].push_back(to);
        }
        //printf("now = %d to = %d depth = %d color = %d\n", now, to, d, color);
        edge[i].vis = true;			//一条边真的只遍历一次
        update(to, d, c, now);
    }
}

void sou_soutmd()
{
    for (int i = 1; i <= n; i ++)
        depth[i] = inf / 10;
    depth[1] = 1; v[1].push_back(1);
    for (int i = 1; i <= n; i++)
    {
        int num = v[i].size();					//vector v[i]代表当前深度i中各种元素
        for (int j = 0; j < num; j++)
        {
            int now = v[i][j];
            for (int k = head[now]; k; k = edge[k].nex)
            {
                int to = edge[k].to;
                if (edge[k].vis) continue;
                if (depth[to] <= i) continue;    //这样保证了点to一定不同色且深度一定是最优
                if (depth[to] != i + 1)
                {
                    depth[to] = i + 1;
                    v[i + 1].push_back(to);
                }
                edge[k].vis = true;			//真的只遍历一次
                update(to, i + 1, edge[k].col, now);
            }
        }
    }
}

最后想想有那时间爆破为啥不去学学真正的图论算法,好傻,就弃了这样的做法。


后来才知道这题的正解,拆点(我喜欢叫拆边),可以说是很巧妙了。
我们首先知道有两点一边u → v(边上颜色为c),我们这么认为这个过程,

对于一个点,我们只关心,它进入了什么颜色,以及它一下步要变成什么颜色。
所以从u点经过颜色c到v点我们可以看成分解成这样的
几个过程:
1.从u这个入口走进颜色c
2.在颜色c中走到出口v
3.从这个出口走到v

把这条边拆成u → uc → vc → v,u到一个点uc走了1个单位距离,从uc到vc是同色无代价,再从vc出v也需要付出1的代价。这条边上的代价就是总共的2除以2为1 。
这样也许你看不出来啥,如果两条边同色边相连呢?
eg. u → v → w(c):
拆成 u → uc → vc → v → vc → wc → w,我们注意到出现了两个vc,根据最短路的松弛规则,u与w之间,uc到vc的最短距离就是0,所以我们可以说u → w的最短路是,u → uc → vc → wc → w,而这个付出的代价仅为2除以2,就可以巧妙的过滤掉了中间同色边上的节点v。不同色就无法松弛,所以能构成正解。
同理就是全图上所有的节点,拆点后最短路就完事了。

拆点时注意可以用map存:

int getid(int x, int y){
    if (!Map[x][y])			//没有出现过,就赋予新点id
        Map[x][y] = ++n;
    return Map[x][y];
}

然后上ac代码吧,感觉也没啥好说的了,图的构建才是关键,思路牛逼震惊我:

#include<bits/stdc++.h>
#define FOR(a, b, c) for(int a=b; a<=c; a++)
#define maxn 5000005					//因为拆了后有很多边,数组一定要开大
#define maxm 55
#define hrdg 1000000007
#define zh 16711680
#define inf 2147483647
#define llinf 9223372036854775807
#define ll long long
#define pi acos(-1.0)
#define ls p<<1
#define rs p<<1|1
using namespace std;

int n, m, u, v, c;
int dis[maxn], goal;
bool vis[maxn];
struct Edge{
    int to, nex, val;
    bool operator < (const Edge & rhs) const			//优先队列内排序规则
    {return val > rhs.val;}
}edge[maxn];
int head[maxn], tot=0;
void add_edge(int u, int v, int c){edge[tot]={v, head[u], c}; head[u]=tot++;}
map<int, int> Map[maxn];
priority_queue<Edge> q;

inline int read(){
    char c=getchar();long long x=0,f=1;
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

int getid(int x, int y){
    if (!Map[x][y])
        Map[x][y] = ++n;
    return Map[x][y];
}

void dijsktra(int start, int goal)			//最短路
{
    for (int i = 1; i <= 10 * n; i++)
        dis[i] = inf;
    dis[start] = 0;
    q.push((Edge){start, 0, 0});
    while (!q.empty())
    {
        Edge k = q.top(); q.pop();
        int now = k.to;
        if (vis[now]) continue;
        vis[now] = 1;
        for (int i= head[now]; ~i; i = edge[i].nex)
        {
            int to = edge[i].to;
            if (dis[to] > dis[now] + edge[i].val)
            {
                dis[to] = dis[now] + edge[i].val;
                q.push((Edge){to, 0, dis[to]});
            }
        }
    }
    if (dis[goal] == inf)				//证明没届到
        puts("-1");
    else
        printf("%d", dis[goal] / 2);
}

int main()
{
    n = read(); m = read();
    goal = n;
    for (int i = 1; i <= n; i++) Map[i].clear();				//好像这步地图清空没有多少必要?
    memset(head, -1, sizeof(head));
    for (int i = 1; i <= m; i++)
    {
        u = read(); v = read(); c = read();
        int uc = getid(u, c);
        int vc = getid(v, c);
        add_edge(uc, vc, 0); add_edge(vc, uc, 0);			//在有了uc和vc的编号的前提下建边,规则已经说过了
        add_edge(u, uc, 1); add_edge(v, vc, 1);
        add_edge(uc, u, 1); add_edge(vc, v, 1);
    }
    dijsktra(1, goal);					//最短路
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值