[SDOI2017]天才黑客

一、题目

点此看题

二、解法

真的毒瘤,我 TM 搞了三个小时,调起来太 TM 爽了。

言归正传,这道题很容易想到一个最短路解法,我们把每条边拆成两个点,它们之间的边权值都是原来的边权,然后对于每个点(原图),我们把入点和出点暴力连边,边权为 l c p lcp lcp(字典树上 l c a lca lca的深度 − 1 -1 1),这个做法可以拿到 0 0 0分的高分,因为入点和出点相连时边数最多是 m 2 m^2 m2,然后时间复杂度就无力回天了。


考虑优化吧,现在的问题在于边数过多,我们能否减少一些边呢?

官方解法用到了权值是 字典树的lca算的 这一性质,我们可以用虚树的思想,我们先用字典树的 d f n dfn dfn将入点和出点排序,然后有一个结论: l c p ( a l , a r ) = min ⁡ i = l r − 1 l c p ( a i , a i + 1 ) lcp(a_l,a_r)=\min_{i=l}^{r-1} lcp(a_i,a_{i+1}) lcp(al,ar)=mini=lr1lcp(ai,ai+1)

我们考虑到最短路会选择最短的,那么我们可以用 d f n dfn dfn相邻的 l c p lcp lcp表示区间的 l c p lcp lcp,具体实现如下:

假设我们已经得到了一些入点和出点(这个例子编号的大小代表 d f n dfn dfn的大小),我们先把相邻的入点和出点连边,边权为 0 0 0
在这里插入图片描述
我们要用 d f n dfn dfn相邻的连边代表所有边,上图应该这样连:
在这里插入图片描述
总结一下方法,就是先排序,将相邻的点连边,设这两个点为 ( x , y ) (x,y) (x,y),找到 d f n dfn dfn小于等于 x x x并且最右边的, d f n dfn dfn大于等于 y y y的最左边的,将他们连边,边权为 l c p ( x , y ) lcp(x,y) lcp(x,y)

上面举的例子只讲了入点小的向出点大的连的方法,事实上还有处理反向的(比如我们上面能处理 1 − > 2 1->2 1>2,但是处理不了 6 − > 2 6->2 6>2),所以我们还有把这个部分扩大两倍,用类似的方法再做一遍。

然后就直接跑 d i j k s t r a dijkstra dijkstra事实证明我写不来dij,一直调不出来 ),贴个代码,有注释。

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 20005;
const int M = 200005;
#define LL long long
#define inf (1ll<<60)
int read()
{
    int x=0,flag=1;
    char c;
    while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
    while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
    return x*flag;
}
int T,n,m,k,Index,tot,id,t,f[M],tmp[M],pos[M],dfn[N],dep[N],fa[N][20];
vector<int> rr[M],cc[M],g[M];
LL dp[M];
struct edge
{
    int v,c,next;
    edge(int V=0,int C=0,int N=0) : v(V), c(C), next(N) {}
} e[M*10];
struct node
{
    int u;
    LL c;
    node(int U=0,LL C=0) : u(U), c(C) {}
    bool operator < (const node &B) const
    {
        return c>B.c;
    }
};
void add(int u,int v,int c)
{
    e[++tot]=edge(v,c,f[u]),f[u]=tot;
}
void ins(int u,int v,int c,int d)
{
    //分两部分,要拆成4个点,13出,24入
    pos[++id]=d;
    add(t+1,t+2,c);
    add(t+1,t+4,c);
    add(t+3,t+2,c);
    add(t+3,t+4,c);
    rr[v].push_back(id);
    cc[u].push_back(id);
    t+=4;
}
int Abs(int x)
{
    return x>0?x:-x;
}
bool cmp(int a,int b)
{
    //dfn排序,加了abs是因为下面打了负数标记
    return dfn[pos[Abs(a)]]<dfn[pos[Abs(b)]];
}
void dfs(int u,int p)//字典树的预处理
{
    dfn[u]=++Index;
    fa[u][0]=p;
    dep[u]=dep[p]+1;
    for(int i=1; i<20; i++)
        fa[u][i]=fa[fa[u][i-1]][i-1];
    for(int i=0; i<g[u].size(); i++)
        if(g[u][i]^p)
            dfs(g[u][i],u);
}
int lca(int u,int v)
{
    if(dep[u]<=dep[v]) swap(u,v);
    for(int i=19; i>=0; i--)
        if(dep[fa[u][i]]>=dep[v])
            u=fa[u][i];
    if(u==v) return u;
    for(int i=19; i>=0; i--)
        if(fa[u][i]^fa[v][i])
            u=fa[u][i],v=fa[v][i];
    return fa[u][0];
}
int get(int u,int v)//lcp
{
    return dep[lca(u,v)]-1;
}
void build(int x)
{
    if(rr[x].empty() || cc[x].empty()) return ;
    sort(rr[x].begin(),rr[x].end(),cmp);
    sort(cc[x].begin(),cc[x].end(),cmp);
    int len=0,tt;
    for(int i=rr[x].size()-1; i>0; i--) add(rr[x][i-1]*4-2,rr[x][i]*4-2,0),add(rr[x][i]*4,rr[x][i-1]*4,0); //相邻的接一起
    for(int i=cc[x].size()-1; i>0; i--) add(cc[x][i-1]*4-3,cc[x][i]*4-3,0),add(cc[x][i]*4-1,cc[x][i-1]*4-1,0);
    for(int i=0; i<cc[x].size(); i++) tmp[++len]=-cc[x][i]; //存下来排序,打负数标记
    for(int i=0; i<rr[x].size(); i++) tmp[++len]=rr[x][i];
    sort(tmp+1,tmp+len+1,cmp);
    for(int t=1,i=0,j=0; t<len; t++)
    {
        if(tmp[t]<0) j++,tmp[t]*=-1;
        else i++;//用打的标记移动下标(i是入,j是出,都是未达到的)
        tt=get(pos[tmp[t]],pos[Abs(tmp[t+1])]);
        if(i!=0 && j!=cc[x].size()) add(rr[x][i-1]*4-2,cc[x][j]*4-3,tt);//分成两个部分
        if(j!=0 && i!=rr[x].size()) add(rr[x][i]*4,cc[x][j-1]*4-1,tt);
    }
}
void dijkstra()
{
    priority_queue<node> q;
    t++;//多建一个出发点,连1的所有出点
    for(int i=cc[1].size()-1; i>=0; i--) add(t,cc[1][i]*4-3,0),add(t,cc[1][i]*4-1,0); //一开始没打等号。。。
    memset(dp,0x3f,sizeof dp);
    dp[t]=0;
    q.push(node(t,0));
    while(!q.empty())
    {
        node t=q.top();
        q.pop();
        if(t.c>dp[t.u]) continue ;
        for(int i=f[t.u]; i; i=e[i].next)
        {
            int v=e[i].v,c=e[i].c;
            if(dp[v]>dp[t.u]+c)
            {
                dp[v]=dp[t.u]+c;
                q.push(node(v,dp[v]));//这里写成c了,还得了25分,样例也没被卡。。。
            }
        }
    }
    for(int i=2; i<=n; i++)
    {
        LL ans=inf;
        for(int j=0; j<rr[i].size(); j++)
            ans=min(ans,dp[rr[i][j]*4]);
        printf("%lld\n",ans);
    }
}
int main()
{
    T=read();
    while(T--)
    {
        n=read();
        m=read();
        k=read();
        tot=id=t=Index=0;
        memset(f,0,sizeof f);
        for(int i=1; i<=k; i++) g[i].clear();
        for(int i=1; i<=n; i++) rr[i].clear(),cc[i].clear();
        for(int i=1; i<=m; i++)
        {
            int u=read(),v=read(),c=read(),d=read();
            ins(u,v,c,d);
        }
        for(int i=1; i<k; i++)
        {
            int u=read(),v=read();
            read();
            g[u].push_back(v);
            g[v].push_back(u);
        }
        dfs(1,0);
        for(int i=2; i<=n; i++)
            build(i);//不用建1,因为1直接出发即可,它的字串是最好的
        dijkstra();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值