洛谷题单指南-最短路-P3275 [SCOI2011] 糖果

原题链接:https://www.luogu.com.cn/problem/P3275

题意解读:n个小朋友分糖果,每个人分得di个,每个人分得的数量满足一组不等式关系,求最少需要多少糖果才能保证所有小朋友都能分到糖果。

解题思路:又是一道典型的差分约束问题,关键在于先确定要求什么,再考虑如何确定不等式关系,进而建图。

题目要求的是最少一共有多少糖果,只需要求出每一个小朋友最少糖果数,再加和即可,也就是求图中每个点的最长路再相加。

为什么每个小朋友的最少糖果数对应最长路?

因为对于一组不等式要求最小值,最终要简化为形如Xi>=a,Xi>=b,Xi>=c...Xi的最小值必然是a、b、c中的最大值,因此要求最长路。

最长路在不等式中的关系是">="。 

如果无法满足所有小朋友的要求,表示不等式无可能解,说明图中存在正环。

对于题目中给出的5种关系,可以分别进行如下转化:

1、如果 , 表示第  个小朋友分到的糖果必须和第  个小朋友分到的糖果一样多

关系为da = db,转化为da >= db,db >= da,建立从a->b,b->a的权值为0的边。

2、如果 , 表示第  个小朋友分到的糖果必须少于第  个小朋友分到的糖果

关系为da < db,转化为db >= da + 1,建立从a->b权值为1的边。

3、如果 , 表示第  个小朋友分到的糖果必须不少于第  个小朋友分到的糖果

关系为da >= db,建立从b->a权值为0的边。

4、如果 , 表示第  个小朋友分到的糖果必须多于第  个小朋友分到的糖果

关系为da > db,转化为da <= db + 1,建立从b->a权值为1的边。

5、如果 , 表示第  个小朋友分到的糖果必须不多于第  个小朋友分到的糖果

关系为da <= db,转化为db >= da,建立从a->b权值为0的边。

注意:由于要保证所有小朋友都能分到糖果,这是一个关键的隐藏条件

对于所有的1<=i<=n,有di > 0

我们可以增加一个0号小朋友,让他分得的糖果数为0,这样就有所有的di > d0

转化为di >= d0 + 1,建立从0到所有1~n的点权值为1的边即可。

另外:本题会卡SPFA,通过使用两个hack技巧之后,仍然会有2个测试点无法通过,但还是会首先给出SPFA求最长路的方法,后面给出正解。

方法一 :SPFA

hack1:用栈替换队列,更快找到环

hack2:限定最大松弛次数,更快判断是环

100分代码(有两个测试点无法通过):
#include <bits/stdc++.h>
using namespace std;

const int N = 100005, M = 300005;
int h[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N], total;
bool vis[N];
int n, m;

void add(int a, int b, int c)
{
    e[++idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
}

//求最长路,判正环
bool spfa()
{
    memset(dist, -0x3f, sizeof(dist));
    dist[0] = 0;
    stack<int> q; //hack1:用栈替换队列,更快找到环
    q.push(0);

    while(q.size())
    {
        int u = q.top(); q.pop();
        vis[u] = false;
        for(int i = h[u]; ~i; i = ne[i])
        {
            int v = e[i];
            if(dist[v] < dist[u] + w[i])
            {
                dist[v] = dist[u] + w[i];
                cnt[v] = cnt[u] + 1;
                if(++total > 10 * n) return true; //hack2:限定最大松弛次数,更快判断是环
                if(cnt[v] >= n + 1) return true; //存在正环
                if(!vis[v])
                {
                    q.push(v);
                    vis[v] = true;
                }
            }
        }
    }
    return false;
}

int main()
{
    cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
    memset(h, -1, sizeof(h));
    cin >> n >> m;
    while(m--)
    {
        int x, a, b;
        cin >> x >> a >> b;
        if(x == 1) add(a, b, 0), add(b, a, 0);
        else if(x == 2) add(a, b, 1);
        else if(x == 3) add(b, a, 0);
        else if(x == 4) add(b, a, 1);
        else add(a, b, 0);
    }
    for(int i = 1; i <= n; i++) add(0, i, 1);
    if(spfa()) cout << -1; 
    else 
    {
        long long ans = 0;
        for(int i = 1; i <= n; i++) ans += dist[i];
        cout << ans;
    }
    return 0;
}

方法二:强联通分量

本题的关键在于:1、判断正环; 2、求最长路。

接着分析,本题建图之后所有权值都是>=0,如果有向图存在环,一定在强联通分量中且该强联通分量必须有一条边权值>0;

如果不存在环,则说明有向图的所有强联通分量内边的权值都==0,如此即可判断是否有正环。

既然一个强连通分量中,所有边都是0,说明强联通分量所有点的值相等,也就是所有点的最长路相等,因此可以对强联通分量进行缩点,

这样就得到一个DAG,对于拓扑图可以通过递推求得最长路。

最后,求答案的时候,要注意一个缩点的最长路要乘上该连通分量内节点数量,最后累加得到结果。

100分代码(AC代码):

#include <bits/stdc++.h>
using namespace std;

const int N = 100005, M = 600005;
int h1[N], h2[N], e[M], w[M], ne[M], idx;
int dfn[N], low[N], timestamp;
stack<int> stk;
bool vis[N];
int scc[N], cnt;
int sz[N];
long long dist[N];

int n, m;

void add(int h[], int a, int b, int c)
{
    e[++idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++timestamp;
    stk.push(u);
    vis[u] = true;

    for(int i = h1[u]; i; i = ne[i])
    {
        int v = e[i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(vis[v])
        {
            low[u] = min(low[u], dfn[v]);
        }
    }

    if(low[u] == dfn[u])
    {
        cnt++;
        while(true)
        {
            int t = stk.top(); stk.pop();
            vis[t] = false;
            scc[t] = cnt;
            sz[cnt]++;
            if(t == u) break;
        }
    }
}

bool build()
{
    for(int i = 0; i <= n; i++) //从0开始的点
    {
        for(int j = h1[i]; j; j = ne[j])
        {
            int u = scc[i], v = scc[e[j]];
            if(u != v)
            {
                add(h2, u, v, w[j]);
            }
            else //i,e[j]在一个连通分量里
            {
                if(w[j] > 0) return true; //存在正环
            }
        }
    }
    return false;
}

int main()
{
    cin >> n >> m;
    while(m--)
    {
        int x, a, b;
        cin >> x >> a >> b;
        if(x == 1) add(h1, a, b, 0), add(h1, b, a, 0);
        else if(x == 2) add(h1, a, b, 1);
        else if(x == 3) add(h1, b, a, 0);
        else if(x == 4) add(h1, b, a, 1);
        else add(h1, a, b, 0);
    }
    for(int i = 1; i <= n; i++) add(h1, 0, i, 1);
    
    tarjan(0); //计算强联通分量

    if(build()) //缩点,判断是否有正环
    {
        cout << -1; 
        return 0;
    }

    //递推最长路
    for(int i = cnt; i >= 1; i--)
    {
        for(int j = h2[i]; j; j = ne[j])
        {
            int v = e[j];
            if(dist[v] < dist[i] + w[j])
            {
                dist[v] = dist[i] + w[j];
            }
        }
    }

    long long ans = 0;
    for(int i = 1; i <= cnt; i++) ans += dist[i] * sz[i];
    cout << ans;
    
    return 0;
}

 

posted @ 2025-04-11 17:41  hackerchef  阅读(29)  评论(0)    收藏  举报