拓扑排序例题

拓扑排序例题:

模板题1:P4017 传送

题意:

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有 1 1 1 秒的时间。

由于这个结果可能过大,你只需要输出总数模上 80112002 80112002 80112002 的结果。

输入:

第一行,两个正整数 n , m n,m n,m,表示生物种类 n n n 和吃与被吃的关系数 m m m

接下来 m m m 行,每行两个正整数,表示被吃的生物A和吃A的生物B。

输出:

一行一个整数,为最大食物链数量模上 80112002 80112002 80112002 的结果。

思路:

记录下所有入度数为0的节点,将他们的初始状态 d p [ i ] dp[i] dp[i] 定义为1.之后对于 x x x 的所有后继节点 y y y ,状态转移方程为: d p [ y ] = m a x ( d p [ x ] + 1 , d p [ y ] ) dp[y]=max(dp[x]+1,dp[y]) dp[y]=max(dp[x]+1,dp[y]) 输出 ∑ i d p [ i ] , i ∈ 出 度 数 为 0 的 节 点 \sum_{i}dp[i],i\in出度数为0的节点 idp[i],i0

#include<bits/stdc++.h>
using namespace std;
#define MAXN 5005
#define ll long long
#define mod 80112002
ll n,m;
vector <int> mp[MAXN];
int indgr[MAXN];
int outdgr[MAXN];
ll dp[MAXN];
ll ans=0;
void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        mp[u].emplace_back(v);
        indgr[v]++;
        outdgr[u]++;
    }
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        if(indgr[i]==0)
        {
            dp[i]=1;
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(auto it:mp[now])
        {
            dp[it]=(dp[it]+dp[now])%mod;
            indgr[it]--;
            if(indgr[it]==0)
            {
                if(outdgr[it]==0) ans=(ans+dp[it])%mod;
                q.push(it);
            }
        }
    }
    cout<<ans<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

模板题2:求最长路 传送

G G G 为有 n n n 个顶点的带权有向无环图, G G G 中各顶点的编号为 1 1 1 n n n,请设计算法,计算图 G G G 1 , n 1,n 1,n 间的最长路径。

分析:

首先这是一个有向无环图,因为如果有环不存在最长路。那么很明显就可以用拓扑遍历进行松弛操作,当 d i s [ n e x t ] < d i s [ n o w ] + e d g e [ i ] . v a l dis[next]<dis[now]+edge[i].val dis[next]<dis[now]+edge[i].val 进行更新。注意将INF赋值为负无穷。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 50005
int head[MAXN];int tot;
struct node
{
    int to,next,val;
}edge[MAXN];
void add_edge(int from,int to,int val)
{
    edge[++tot].to=to;edge[tot].val=val;edge[tot].next=head[from];head[from]=tot;
}
#define INF -114514114514
int indgr[MAXN];ll dp[MAXN];//1到i的最长路
ll n,m;

void topp()
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        if(indgr[i]==0) q.push(i);
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int next=edge[i].to;
            int val=edge[i].val;
            if(dp[now]+val>dp[next])
            {
                dp[next]=dp[now]+val;
            }
            indgr[next]--;
            if(indgr[next]==0) q.push(next);
        }
    }
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        cin>>u>>v>>w;
        add_edge(u,v,w);
        indgr[v]++;
    }
    for(int i=2;i<=n;i++) dp[i]=INF;
    topp();
    if(dp[n]==INF) cout<<-1<<endl;
    else cout<<dp[n]<<endl;
}


int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

郁闷的记者 (对于拓扑序的理解)传送

你是一个体育报社的记者,你接受到一个艰难的任务:有N支足球队参加足球比赛,现在给你一些比赛的结果,需要你给出各支球队的排名,从1到N。

以下是给你的一些信息:

(1)没有平局;

(2)不同的球队排名不能相同;

(3)对于所有满足l≤a<b≤n,第a名的球队一定可以打败第b名的球队。

给你部分比赛结果,要求给出排名,并且判断是否存在另一种排名方法满足给你的比赛结果。

分析:

什么叫存在令一种排名方法满足比赛结果呢?是不是可以理解为会有并列排名的选手出现。比如1号选手和3号选手都能打败2号选手,但也都不能打败4号选手。将这种抽象为图即是这样的结果

graph(3)

可以发现,转化为图后,1号和3号在拓扑排序中就意味着同一级别,换句话说就是能在 一次遍历中同时入队

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 5005
ll n,m;
//保证没有自环,同一级别的入队次数只能为1
vector <ll> mp[MAXN];
ll indgr[MAXN];
int flag=0;
vector <int> ans;
void build()
{
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        mp[u].emplace_back(v);
        indgr[v]++;
    }
}

void topp()
{
    queue <int> q;
    int cnt=0;//记录同一级别的人的个数,大于1就有多种排名情况
    for(int i=1;i<=n;i++)
    {
        if(indgr[i]==0)
        {
            cnt++;
            q.push(i);
            ans.emplace_back(i);
        }
    }
    if(cnt>1) flag=1;
    while(!q.empty())
    {
        cnt=0;
        int now=q.front();q.pop();
        for(auto it:mp[now])
        {
            indgr[it]--;
            if(indgr[it]==0)
            {
                cnt++;
                ans.emplace_back(it);
                q.push(it);
            }
        }
        if(cnt>1) flag=1;
    }
}

void output()
{
    for(auto x:ans) cout<<x<<endl;
    cout<<flag;
}

void solve()
{
    cin>>n>>m;
    build();
    topp();
    output();
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

摄像头 (对入度的理解) 传送

食品店里有 n n n 个摄像头,这种摄像头很笨拙,只能拍摄到固定位置。现有一群胆大妄为的松鼠想要抢劫食品店,为了不让摄像头拍下他们犯罪的证据,他们抢劫前的第一件事就是砸毁这些摄像头。

为了便于砸毁摄像头,松鼠歹徒们把所有摄像头和摄像头能监视到的地方统一编号,一个摄像头能被砸毁的条件是该摄像头所在位置不被其他摄像头监视。

现在你的任务是帮松鼠们计算是否可以砸掉所有摄像头,如不能则输出还没砸掉的摄像头的数量。

输入:

1 1 1 行,一个整数 n n n ,表示摄像头的个数。

2 2 2 n + 1 n+1 n+1 行是摄像头的信息,包括:摄像头的位置 x x x ,以及这个摄像头可以监视到的位置数 m m m ,之后 m m m 个数 y y y 是此摄像头可以监视到的位置。(砸了这些摄像头之后自然这些位置就监视不到了)

分析:

根据这些摄像机组成的监视网络构建一张有向无环图,遍历它。遍历完成后,那些入度数为 0 0 0 的摄像头位置就可以砸掉了,因为能监控这个摄像头的所有摄像头(可以没有任何摄像头)也可以被砸掉。这题坑的地方就是有的地方不一定有摄像头,得开个 v i s i t visit visit 记录一下

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 505
vector <int> mp[MAXN];
int indgr[MAXN];//有些入度大于0的地方可能没有摄像头
int visit[MAXN];
ll n,m;

void build()
{
    for(int i=1;i<=n;i++)
    {
        int id;
        cin>>id>>m;
        visit[id]=1;
        while(m--)
        {
            int to;
            cin>>to;
            mp[id].emplace_back(to);
            indgr[to]++;
        }
    }
}

void topp()
{
    int del=0;
    queue <int> q;
    for(int i=1;i<=n;i++) if(indgr[i]==0&&visit[i]==1) 
    {
        q.push(i);
        del++;
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(auto it:mp[now])
        {
            indgr[it]--;
            if(indgr[it]==0&&visit[it]==1) 
            {
                q.push(it);
                del++;
            }
        }
    }
    if(del==n) cout<<"YES"<<endl;
    else cout<<n-del<<endl;
}

void solve()
{
    cin>>n;
    build();
    topp();
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

Timeline G (建图的哲学) 传送

Bessie 在过去的 M M M 天内参加了 N N N 次挤奶。但她已经忘了她每次挤奶是在哪个时候了。

对于第 i i i 次挤奶,Bessie 记得它不早于第 S i S_i Si 天进行。另外,她还有 C C C 条记忆,每条记忆形如一个三元组 ( a , b , x ) (a,b,x) (a,b,x) ,含义是第 b b b 次挤奶在第 a a a 次挤奶结束至少 x x x 天后进行。

现在请你帮 Bessie 算出在满足所有条件的前提下,每次挤奶的最早日期。

保证 Bessie 的记忆没有错误,这意味着一定存在一种合法的方案,使得:

  • i i i 次挤奶不早于第 S i S_i Si 天进行,且不晚于第 M M M 天进行;
  • 所有的记忆都得到满足;

样例1:

4 10 3
1 2 3 4
1 2 5
2 4 2
3 4 4

分析:

她还有 C C C 条记忆,每条记忆形如一个三元组 ( a , b , x ) (a,b,x) (a,b,x) ,含义是第 b b b 次挤奶在第 a a a 次挤奶结束至少 x x x 天后进行。对于题目给出的这个条件,我们可以很容易的建图,像这样

graph(4)

但另外一个条件我们怎么利用呢?想象一下,是不是可以新建一个超级源点 0 0 0 ,这个点的入度为0,和每个其他的节点都有一条出边,这样就可以完成图的构建,进而进行拓扑排序了。

graph(5)

转移方程为 d p [ n e x t ] = m a x ( d p [ n o w ] + e d g e [ i ] . v a l , d p [ n e x t ] ) dp[next]=max(dp[now]+edge[i].val,dp[next]) dp[next]=max(dp[now]+edge[i].val,dp[next])

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define ll long long
int indgr[MAXN<<1];int head[MAXN<<1]; ll tim[MAXN<<1];
struct node
{
    int to,next,val;

}edge[MAXN<<1];
int cnt=0;
void add_edge(int from,int to,int val)
{
    edge[++cnt].val=val;edge[cnt].to=to;edge[cnt].next=head[from];head[from]=cnt;
}
ll n,m,k;

void solve()
{
    cin>>n>>k>>m;
    for(int i=0;i<=n;i++) 
    {
        head[i]=-1;
        tim[i]=-1;
    }
    tim[0]=0;
    for(int i=1;i<=n;i++)
    {
        int temp;
        cin>>temp;
        add_edge(0,i,temp);
        indgr[i]++;
    }
    for(int i=1;i<=m;i++)
    {
        int to,from,val;
        cin>>from>>to>>val;
        add_edge(from,to,val);
        indgr[to]++;
    }
    queue <int> q;
    q.push(0);
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i!=-1;i=edge[i].next)
        {
            int val=edge[i].val;
            int j=edge[i].to;
            tim[j]=max(tim[j],tim[now]+val);
            indgr[j]--;
            if(indgr[j]==0) q.push(j);
        }
    }
    for(int i=1;i<=n;i++) cout<<tim[i]<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

菜肴制作(建图的智慧)传送

知名美食家小 A 被邀请至 ATM 大酒店,为其品评菜肴。ATM 酒店为小 A 准备了 n n n 道菜肴,酒店按照为菜肴预估的质量从高到低给予 1 1 1 n n n 的顺序编号,预估质量最高的菜肴编号为 1 1 1

由于菜肴之间口味搭配的问题,某些菜肴必须在另一些菜肴之前制作,具体的,一共有 m m m 条形如 i i i 号菜肴必须先于 j j j 号菜肴制作的限制,我们将这样的限制简写为 ( i , j ) (i,j) (i,j)

现在,酒店希望能求出一个最优的菜肴的制作顺序,使得小 A 能尽量先吃到质量高的菜肴:

例 1:共 4 4 4 道菜肴,两条限制 ( 3 , 1 ) (3,1) (3,1) ( 4 , 1 ) (4,1) (4,1),那么制作顺序是 3 , 4 , 1 , 2 3,4,1,2 3,4,1,2

例 2:共 5 5 5 道菜肴,两条限制 ( 5 , 2 ) (5,2) (5,2) ( 4 , 3 ) (4,3) (4,3),那么制作顺序是 1 , 5 , 2 , 4 , 3 1,5,2,4,3 1,5,2,4,3

分析:

很自然的想到按照字典序进行拓扑排序,但是仔细想想就是错的,比如 ( 2 , 4 ) , ( 3 , 1 ) (2,4),(3,1) (2,4),(3,1) 这组限制条件。题目要求的最优解为 3 , 1 , 2 , 4 3,1,2,4 3,1,2,4 ,但是做出来确是 2 , 3 , 1 , 4 2,3,1,4 2,3,1,4 的效果 。

继续考虑,如果说要把字典序小的先安排是不是就相当于把字典序大的尽可能往后安排呢。

这样我们就可以构造一个反图,每次让最大的入队(用大顶堆实现),这样就能保证小的尽可能晚的入队,满足了题意,到最后反着输出这个序列就行了。

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define ll long long
ll n,m;
int head[MAXN];int cnt=0;
int indgr[MAXN];
vector <int> ans;
struct node
{
    int to,next;
}edge[MAXN];
void add_edge(int from,int to)
{
    edge[++cnt].to=to;edge[cnt].next=head[from];head[from]=cnt;
}
void init()
{
    cnt=0;
    ans.clear();
    for(int i=1;i<=n;i++) 
    {
        indgr[i]=0;
    }
    for(int i=1;i<=m;i++)
    {
        head[i]=0;
        edge[i].to=0;edge[i].next=0;
    }
}
void build()
{
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(v,u);
        indgr[u]++;
    }
}
void topp()
{
    priority_queue <int> q;
    for(int i=n;i>=1;i--)
    {
        if(indgr[i]==0) 
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now=q.top();q.pop();
        ans.emplace_back(now);
        for(int i=head[now];i;i=edge[i].next)
        {
            int j=edge[i].to;
            indgr[j]--;
            if(indgr[j]==0)
            {
                //ans.emplace_back(j);
                q.push(j);
            }
        }
    }
}
void output()
{
    if(ans.size()!=n) cout<<"Impossible!";
    else 
    {
       for(int i=ans.size()-1;i>=0;i--) cout<<ans[i]<<" ";
    }
    cout<<endl;
}
void solve()
{
    cin>>n>>m;
    init();
    build();
    topp();
    output();
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    int T;
    cin>>T;
    while(T--)
    {
        solve();
    }
    return 0;
}

旅行计划 (拓扑+DP板子)传送

小明要去一个国家旅游。这个国家有 N N N 个城市,编号为 1 1 1 N N N ,并且有 M M M 条道路连接着,小明准备从其中一个城市出发,并只往东走到城市 i i i 停止。

所以他就需要选择最先到达的城市,并制定一条路线以城市 i i i 为终点,使得线路上除了第一个城市,每个城市都在路线前一个城市东面,并且满足这个前提下还希望游览的城市尽量多。

现在,你只知道每一条道路所连接的两个城市的相对位置关系,但并不知道所有城市具体的位置。现在对于所有的 i i i ,都需要你为小明制定一条路线,并求出以城市 i i i 为终点最多能够游览多少个城市。

分析:

状态转移和那道最长路有点像,而且权值还是1,反而更简单了 这居然是个绿题

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MAXM 200005
#define ll long long
ll n,m;
ll head[MAXM];int cnt=0;
ll indgr[MAXN];
ll dp[MAXN];
struct node
{
    int to,next;
}edge[MAXM];
void add_edge(int from,int to)
{
    edge[++cnt].to=to;edge[cnt].next=head[from];head[from]=cnt;
}
void init()
{
    for(int i=1;i<=n;i++) 
    {
        dp[i]=0;
        indgr[i]=0;
    }
}
void build()
{
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        add_edge(u,v);
        indgr[v]++;
    }
}
void topp()
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        if(indgr[i]==0)
        {
            dp[i]=1;
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int j=edge[i].to;
            dp[j]=max(dp[j],dp[now]+1);
            indgr[j]--;
            if(indgr[j]==0) q.push(j);
        }
    }
}
void output()
{
    for(int i=1;i<=n;i++) cout<<dp[i]<<endl;
}
void solve()
{
    cin>>n>>m;
    init();
    build();
    topp();
    output();
}


int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

神经元(拓扑+DP板子) 传送

一个神经元有一些入边,出边组成。内部有一个阈值 U i U_i Ui 和一个值为 C i C_i Ci C i C_i Ci 表示神经元目前的状态, C i C_i Ci 大于0时,神经元会向下传递信息

规定, C i C_i Ci 服从公式:(其中 n n n 是网络中所有神经元的数目)

C i = ∑ ( j , i ) ∈ E W j , i C j − U i C_i=\sum_{(j,i)\in{E}}W_{j,i}C_j-U_i Ci=(j,i)EWj,iCjUi

公式中的 W j , i W_{j,i} Wj,i(可能为负值)表示连接 j j j 号神经元和 i i i 号神经元的边的权值。当 C i C_i Ci 大于 0 0 0 时,该神经元处于兴奋状态,否则就处于平静状态。当神经元处于兴奋状态时,下一秒它会向其他神经元传送信号,信号的强度为

C i C_i Ci

分析:

转移方程为: c [ j ] = c [ j ] + e d g e [ i ] . v a l ∗ c [ n o w ] c[j]=c[j]+edge[i].val*c[now] c[j]=c[j]+edge[i].valc[now] ,再在入队的时候判断一下 c j c_j cj 是否大于0即可。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 105
#define MAXM MAXN*MAXN
ll n,m;
ll u[MAXN];//每个节点一开始的阈值
ll c[MAXN];//ci=sum(边权*cj)-ui
int indgr[MAXN];
int outdgr[MAXN];
struct node
{
    int to,next,val;
}edge[MAXM];
int head[MAXM];int cnt;
void add_edge(int from,int to,int val)
{
    edge[++cnt].to=to;edge[cnt].val=val;edge[cnt].next=head[from];head[from]=cnt;
}

void topp()
{
    queue <int> q;
    for(int i=1;i<=n;i++) 
    {
        if(c[i]>0&&indgr[i]==0) q.push(i);
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int j=edge[i].to;
            indgr[j]--;
            c[j]=c[j]+edge[i].val*c[now];
            if(indgr[j]==0) 
            {
                c[j]-=u[j];
                if(c[j]>0) q.push(j);
            }
        }
    }
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>c[i]>>u[i];
    }
    //ci大于0并且入度数为0就入队
    for(int i=1;i<=m;i++)
    {
        int u,v,val;
        cin>>u>>v>>val;
        add_edge(u,v,val);
        indgr[v]++;
        outdgr[u]++;
    }
    topp();
    vector <pair<int,int>> ans;
    int f=0;
    for(int i=1;i<=n;i++)
    {
        if(outdgr[i]==0&&c[i]>0) 
        {
            f=1;
            ans.emplace_back(make_pair(i,c[i]));
        }
    }
    if(f==0) cout<<"NULL"<<endl;
    else for(auto x:ans) cout<<x.first<<" "<<x.second<<endl;
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

分割线,高能开始


绿豆蛙的归宿 (期望DP)传送

给出张 n n n 个点 m m m 条边的有向无环图,起点为 1 1 1,终点为 n n n,每条边都有一个长度,并且从起点出发能够到达所有的点,所有的点也都能够到达终点。

绿豆蛙从起点出发,走向终点。 到达每一个顶点时,如果该节点有 k k k 条出边,绿豆蛙可以选择任意一条边离开该点,并且走向每条边的概率为 1 k \frac{1}{k} k1 。现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?

分析:

正推和逆推都可以做,逆推式子要简单点但难想

正推:

对于一条从 x x x y y y 的边

E ( x ) = p 1 x 1 + p 2 x 2 + . . . . . . + p n x n E(x)=p_1x_1+p_2x_2+......+p_nx_n E(x)=p1x1+p2x2+......+pnxn

E ( y ) = p 1 ( x 1 + w ) + p 2 ( x 2 + w ) + . . . . . . + p n ( x n + w ) = E ( x ) + ∑ i = 1 n p i × w ≠ E ( x ) + w E(y)=p_1(x_1+w)+p_2(x_2+w)+......+p_n(x_n+w)=E(x)+\sum_{i=1}^np_i\times w \not= E(x)+w E(y)=p1(x1+w)+p2(x2+w)+......+pn(xn+w)=E(x)+i=1npi×w=E(x)+w

1 1 1 i i i ,所有概率和 i i i 不为1

d p [ i ] dp[i] dp[i] 表示从 1 1 1 i i i 的期望, g [ i ] g[i] g[i] 表示从 1 1 1 i i i 的概率,方程的转移:对于一条从 x x x y y y 的边

d p [ y ] = ∑ i = 1 i n d g r [ y ] ( d p [ x ] + e d g e [ i ] × g [ x ] ) / o u t [ x ] dp[y]=\sum_{i=1}^{indgr[y]}(dp[x]+edge[i] \times g[x])/out[x] dp[y]=i=1indgr[y](dp[x]+edge[i]×g[x])/out[x]

逆推:

E ( y ) = p 1 x 1 + p 2 x 2 + . . . . . . + p n x n E(y)=p_1x_1+p_2x_2+......+p_nx_n E(y)=p1x1+p2x2+......+pnxn

E ( x ) = p 1 ( x 1 + w ) + p 2 ( x 2 + w ) + . . . . . . + p n ( x n + w ) = E ( y ) + ∑ i = 1 n p i × w = E ( y ) + w E(x)=p_1(x_1+w)+p_2(x_2+w)+......+p_n(x_n+w)=E(y)+\sum_{i=1}^{n}p_i \times w=E(y)+w E(x)=p1(x1+w)+p2(x2+w)+......+pn(xn+w)=E(y)+i=1npi×w=E(y)+w

i i i n n n ,所有概率和为1

d p [ i ] dp[i] dp[i] 表示从 i i i n n n 的期望,方程转移:对于一条从 x x x y y y 的边

d p [ x ] = ∑ i = 1 o u t [ x ] ( d p [ y ] + e d g e [ i ] ) / o u t [ x ] dp[x]=\sum_{i=1}^{out[x]}(dp[y]+edge[i])/out[x] dp[x]=i=1out[x](dp[y]+edge[i])/out[x]

只给出一种正推的写法

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
vector <int> mp[MAXN];
#define ll long long
double ans=0;
int cnt=0;int head[MAXN];
int indgr[MAXN];
ll n,m;
struct edge
{
    int to,next,from;
    double val;
}edge[MAXN];
void add_cnt(int from,int to,double val)
{
    edge[++cnt].to=to;edge[cnt].val=val;edge[cnt].next=head[from];head[from]=cnt;
}

void topp()
{
    queue <int> q;
    q.push(1);
    while(!q.empty())
    {
        int now=q.front();q.pop();
        cout<<"now: "<<now<<endl;
        double son=mp[now].size();
        cout<<now<<"outdgr: "<<son<<endl;
        for(int i=head[now];i;i=edge[i].next)
        {
            ans=ans+(1.0)/(son+0.0)*edge[i].val+0.0;
            indgr[edge[i].to]--;
            if(indgr[edge[i].to]==0) q.push(edge[i].to);
        }
    }
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int from,to;
        double val;
        cin>>from>>to>>val;
        add_cnt(from,to,val);
        indgr[to]++;
        mp[from].emplace_back(to);
    }
    topp();
   
    cout<<ans<<endl;
}

int main()
{
    //ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

排序 (拓扑的大型模拟)传送

一个不同的值的升序排序数列指的是一个从左到右元素依次增大的序列,例如,一个有序的数列 A , B , C , D A,B,C,D A,B,C,D 表示 A < B , B < C , C < D A<B,B<C,C<D A<B,B<C,C<D。在这道题中,我们将给你一系列形如 A < B A<B A<B 的关系,并要求你判断是否能够根据这些关系确定这个数列的顺序。

  • 根据前 x x x 个关系即可确定这 n n n 个元素的顺序
  • 根据前 x x x 个关系即发现存在矛盾
  • 根据这 m m m 个关系无法确定这 n n n 个元素的顺序

分析:

数据范围是真的小,说明我们每建一条边就要进行一次拓扑排序,反正也T不了。

再看三种情况,第一种是有稳定顺序,第二种是出现了环,第三个是无环但也没有稳定的拓扑序。

依次分析

第一个问题,有稳定拓扑序说明拓扑排序的层数是 n n n 。也就是说,拓扑的过程中一定能出现一条长度为 n n n 的链。我们只要看最大的层数是不是 n n n 就可以了。

第二个问题,有没有成环。拓扑排序很容易判断,如果没能成功遍历所有点,就说明存在一个环。

第三个问题,看似很难,实际上就是除了第一种和第二种以外的其他情况。

接下来就开始漫长的模拟。

声明一些变量意义:

o r i g i n a l i n d g r originalindgr originalindgr 表示总的图的入度, i n d g r indgr indgr为每次单次遍历用的临时入度。

s e t set set s s ss ss 有两个用途,一个是统计当前所有字母去重后的数量,一个是之后在拓扑遍历后用来标记哪些字母已出现过

s u m sum sum 统计单次拓扑遍历过的点,用于判环

a n s ans ans 表示单次拓扑的最长链,用于判断是否拥有一个稳定的拓扑序

#include<bits/stdc++.h>
using namespace std;
#define MAXN 30
#define ll long long
int indgr[MAXN];int original_indgr[MAXN];
vector <int> mp[MAXN];
set <int> ss;//用于获得当前元素个数
ll n,m;
ll sum,ans;//遍历的点的数量和拓扑链的最大层数
struct node
{
    int u;
    ll val;//层数
};
int step;
int have;

void output()
{
    queue <int> q;
    memset(indgr,0,sizeof(indgr));
    for(int i=0;i<26;i++)
    {
        for(auto j:mp[i])
        {
            indgr[j]++;
        }
    }
    for(int i=0;i<26;i++)
    {
        if(indgr[i]==0&&ss.count(i))
        {
            q.push(i);
            cout<<(char)(i+'A');
        }
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(auto j:mp[now])
        {
            indgr[j]--;
            if(indgr[j]==0)
            {
                cout<<(char)('A'+j);
                q.push(j);
            }
        }
    }
    
}

void topp()
{
    queue <node> q;
    for(int i=0;i<26;i++)
    {
        if(indgr[i]==0&&ss.count(i))//首先必须得保证这个字母出现过
        {
            q.push(node{i,1});
            sum++;
        }
    }
    while(!q.empty())
    {
        node now=q.front();q.pop();
        int u=now.u;ll val=now.val;
        for(auto v:mp[u])
        {
            indgr[v]--;
            if(indgr[v]==0)
            {
                sum++;
                q.push(node{v,val+1});
                ans=max(val+1,ans);
            }
        }
    }
    if(ans==n)//最长拓扑链长度为n
    {
        printf("Sorted sequence determined after %d relations: ",step);
        output();
        cout<<".";
        exit(0);
    }
    if(sum!=have)//有环
    {
        printf("Inconsistency found after %d relations.",step);
        exit(0);
    }
    
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        step=i;
        string s;
        cin>>s;
        char a=s[0];char c=s[2];
        mp[a-'A'].emplace_back(c-'A');
        ss.insert(a-'A');ss.insert(c-'A');
        have=ss.size();
        original_indgr[c-'A']++;
        sum=0;ans=0;
        memcpy(indgr,original_indgr,sizeof(original_indgr));
        topp();
    }
    printf("Sorted sequence cannot be determined.");
}

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

放风筝 (拓扑缩点后DP)传送

总体上就是普通的拓扑DP的意思,但是环可以看做一个节点,且权值为环内所有节点 v a l u e value value 的和

分析:

去他妈的环,先不管他。如果我们不考虑环的情况,这就是很 白菜 的题目。

我们可以采用拓扑排序的方法,遍历整个图,然后对于每个路径维护一下到当前点的最大距离,顺便维护一下这个路径上的最大值。

有环吗,很简单 , T a r j a n Tarjan Tarjan 缩点吗。

这道题强连通分量维护的是包含的元素的点权和。

具体亿些细节看代码吧,毕竟写的第一道缩点题,注释都是拉满的。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define MAXN 200005
#define MAXM 500005
ll n,m;
ll value[MAXN];//存放每个原始点的点权
ll key[MAXN];//表示这个点位于哪个强联通分量
vector <int> original_mp[MAXN];//原始图,带环
vector <int> mp[MAXN];//缩点后的图
int indgr[MAXN];//缩点后的入度数
struct node
{
    ll mx;
    ll sum;
}sccc[MAXN];//强联通分量维护两个信息,整个强连通分量的最大值和缩点后的点权
stack <int> s;
int visit[MAXN];//标记是否在栈中
int dfn[MAXN];//第一次遍历得到的时间戳
int low[MAXN];//最小时间戳,用于判断是不是强连通分量的根
int num;//时间戳
int cnt;//强联通分量个数
ll dp[MAXN][2];//0表示到i点的最长路径之和,1表示路径上的最大点权

void tarjan(int x)
{
    num++;//时间加一
    dfn[x]=num;low[x]=num;//记录时间戳
    visit[x]=1;
    s.push(x);//入栈及标记
    for(auto j:original_mp[x])//遍历他的儿子们
    {
        if(dfn[j]==0) 
        {
            tarjan(j);//如果这个儿子的强联通分量没更新过,就更新一下,然后更新x的最小时间戳
            low[x]=min(low[j],low[x]);
        }
        else if(visit[j]==1)//如果已经遍历过了并且在栈中
        {
            low[x]=min(dfn[j],low[x]);
        }
    }
    if(dfn[x]==low[x])//当前这个点是强联通分量的根,那么就维护这个强联通分量
    {
        cnt++;
        int now=-1;
        while(x!=now)
        {
            now=s.top();s.pop();
            visit[now]=0;
            key[now]=cnt;
            sccc[cnt].mx=max(sccc[cnt].mx,value[now]);
            sccc[cnt].sum+=value[now];
        }
    }
}

void build()//缩点,建新图
{
    for(int i=1;i<=n;i++)
    {
        if(dfn[i]==0) tarjan(i);
    }
    //先维护这张图的所有强连通分量
    //下面开始缩点
    for(int i=1;i<=n;i++)
    {
        for(auto j:original_mp[i])
        {
            if(key[i]==key[j])//原来边中的两个点同属于一个强连通分量,就不连边
            continue;
            //维护缩点后的新图
            mp[key[i]].emplace_back(key[j]);
            indgr[key[j]]++;
        }
    }
}

void topp()
{
    queue <int> q;
    for(int i=1;i<=cnt;i++)
    {
        dp[i][0]=sccc[i].sum;
        dp[i][1]=sccc[i].mx;
    }
    for(int i=1;i<=cnt;i++)
    {
        if(indgr[i]==0) q.push(i);
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(auto next:mp[now])//遍历图
        {
            if(dp[now][0]+sccc[next].sum>dp[next][0])//更新最大路径和
            {
                dp[next][0]=dp[now][0]+sccc[next].sum;
                dp[next][1]=max(sccc[next].mx,dp[now][1]);//更新最大值
            }
            else if(dp[now][0]+sccc[next].sum==dp[next][0])//如果有两条路径和最大的路径
            {
                //最大值选两者中最大的那个(相当于选择了那条路径)
                dp[next][1]=max(dp[now][1],dp[next][1]);
            }
            indgr[next]--;
            if(indgr[next]==0) q.push(next);
        }
    }
}

void output()
{
    int ans=1;
    for(int i=1;i<=cnt;i++)
    {
        if(dp[i][0]>dp[ans][0]||(dp[i][0]==dp[ans][0]&&dp[i][1]>dp[ans][1]))
        {
            ans=i;
        }
    }
    cout<<dp[ans][0]<<" "<<dp[ans][1]<<endl;
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>value[i];
        key[i]=i;//初始化,类似于并查集
    }
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        original_mp[u].emplace_back(v);
    }
    build();//缩点建新图
    topp();
    output();
}


int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}

分糖果 (拓扑缩点,建图的玄妙与精密)传送

输入的第一行是两个整数 N N N K K K 。接下来 K K K 行,表示这些点需要满足的关系,每行 333 个数字, X X X A A A B B B

  • 如果 X = 1 X=1 X=1 , 表示第 A A A 个小朋友分到的糖果必须和第 B B B 个小朋友分到的糖果一样多;
  • 如果 X = 2 X=2 X=2 , 表示第 A A A 个小朋友分到的糖果必须少于第 B B B 个小朋友分到的糖果;
  • 如果 X = 3 X=3 X=3 , 表示第 A A A 个小朋友分到的糖果必须不少于第 B B B 个小朋友分到的糖果;
  • 如果 X = 4 X=4 X=4 , 表示第 A A A 个小朋友分到的糖果必须多于第 B B B 个小朋友分到的糖果;
  • 如果 X = 5 X=5 X=5 , 表示第 A A A 个小朋友分到的糖果必须不多于第 B B B 个小朋友分到的糖果;

输出老师至少需要准备的糖果数量,如果不能满足小朋友们所有要求,输出-1。

分析:

很明显这题还是有环。我们想象以下什么情况会构成环,答案是这个集团内所有人的元素只能是同一个值。

既然这样的话,我们尝试先把所有可能出现糖果相同的边连起来,将这些边标记为 t r u e true true 然后缩点。这样构成一个有向无环图。

接着加入2类和4类边,并把这些边的标记标记为 f a l s e false false 。这时候就会出现非法情况了。

  • 2类边和4类边出现自环

  • 后来拓扑排序遍历的过程中出现了环( 1 , 3 , 5 1,3,5 1,3,5 的情况已经不可能出现环了,说明 2 , 4 2,4 2,4 的加入让图出现了环)

没有啥大问题后进行一个扑的拓,过程中完成 D P DP DP 统计出结果

下面声明一些变量的意义:

s c c scc scc 表示强连通分量的个数, s c c c [ i ] sccc[i] sccc[i]维护了各个强连通分量的族群大小和糖果数量

e d g e edge edge中的 s a m e same same 表示这条边是否允许两边糖果相同,如果能够相同,转移方程为 s c c c [ j ] . c a n d y = m a x ( s c c c [ j ] . c a n d y , s c c c [ n o w ] . c a n d y ) sccc[j].candy=max(sccc[j].candy,sccc[now].candy) sccc[j].candy=max(sccc[j].candy,sccc[now].candy) ;否则为

s c c c [ j ] . c a n d y = m a x ( s c c c [ n o w ] . c a n d y + 1 , s c c c [ j ] . c a n d y ) sccc[j].candy=max(sccc[now].candy+1,sccc[j].candy) sccc[j].candy=max(sccc[now].candy+1,sccc[j].candy)

#include<bits/stdc++.h>
using namespace std;
#define MAXN 100005
#define MAXM 100005
#define ll long long

ll n,m;

struct EDGE
{
    int to,next;
    bool same;//记录该边是否允许两边相等
}original_edge[MAXN],edge[MAXM];
int original_head[MAXM],head[MAXM],original_tot,tot;
void original_add_edge(int from,int to,bool same)
{
    original_tot++;original_edge[original_tot].to=to;original_edge[original_tot].next=original_head[from];original_head[from]=original_tot;
    original_edge[original_tot].same=same;
}
void add_edge(int from,int to,bool same)
{
    tot++;edge[tot].to=to;edge[tot].next=head[from];head[from]=tot;
    edge[tot].same=same;
}

struct relation
{
    int f;int a;int b;
}relation[MAXM];

int scc;//强连通分量个数
int num;//时间戳
int visit[MAXN],low[MAXN],dfn[MAXN],key[MAXN];
stack <int> st;
struct node
{
    ll size=0;//糖果相同的人的数量
    ll candy=0;//糖果的数量
}sccc[MAXN];
int indgr[MAXN];
void tarjan(int x)
{
    num++;
    low[x]=num;dfn[x]=num;
    st.push(x);visit[x]=1;
    for(int i=original_head[x];i;i=original_edge[i].next)
    {
        int j=original_edge[i].to;
        if(dfn[j]==0)
        {
            tarjan(j);
            low[x]=min(low[x],low[j]);
        }
        else if(visit[j]==1)
        {
            low[x]=min(low[x],dfn[j]);
        }
    }
    if(low[x]==dfn[x])
    {
        int now=-1;
        scc++;
        while(now!=x)
        {
            now=st.top();st.pop();
            sccc[scc].size++;
            key[now]=scc;
            visit[now]=0;       
        }
    }
}

void build()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=original_head[i];j;j=original_edge[j].next)
        {
            int jj=original_edge[j].to;
            if(key[i]==key[jj]) continue;
            add_edge(key[i],key[jj],true);
            indgr[key[jj]]++;
        }
    }
    for(int i=1;i<=m;i++)
    {
        if(relation[i].f==2)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(key[relation[i].a],key[relation[i].b],false);
            indgr[key[relation[i].b]]++;
        }
        else if(relation[i].f==4)
        {
            if(key[relation[i].a]==key[relation[i].b])//之前已经定义为糖果相同,现在又要求糖果不同
            {
                cout<<-1<<endl;
                exit (0);
            }
            add_edge(key[relation[i].b],key[relation[i].a],false);
            indgr[key[relation[i].a]]++;
        }
    }
}

void topp()
{
    queue <int> q;
    for(int i=1;i<=scc;i++)
    {
        if(indgr[i]==0)
        {
            sccc[i].candy=1;
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now=q.front();q.pop();
        for(int i=head[now];i;i=edge[i].next)
        {
            int j=edge[i].to;
            if(edge[i].same==true)//允许两边糖果数量相等
            {
                sccc[j].candy=max(sccc[j].candy,sccc[now].candy);
            }
            else if(edge[i].same==false)//不允许两个强连通分量糖果相等
            {
                sccc[j].candy=max(sccc[now].candy+1,sccc[j].candy);
            }
            indgr[j]--;
            if(indgr[j]==0) q.push(j);
        }
    }
    for(int i=1;i<=scc;i++)
    {
        if(indgr[i]!=0)//出现了自环
        {
            cout<<-1<<endl;
            exit (0);
        }
    }
}

void output()
{
    ll ans=0;
    for(int i=1;i<=scc;i++) 
    {
        ans=ans+sccc[i].candy*sccc[i].size;
    }
    cout<<ans<<endl;
}

void solve()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>relation[i].f>>relation[i].a>>relation[i].b;
        if(relation[i].f==1)//将所有权值相同的点缩成一点
        {
            original_add_edge(relation[i].a,relation[i].b,true);
            original_add_edge(relation[i].b,relation[i].a,true);
        }
        else if(relation[i].f==3)
        {
            original_add_edge(relation[i].b,relation[i].a,true);
        }
        else if(relation[i].f==5)
        {
            original_add_edge(relation[i].a,relation[i].b,true);
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(dfn[i]==0) tarjan(i);
    }
    build();
    topp();
    output();
}   

int main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    solve();
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值