• AcWing 2128. 狡猾的商人


    题目传送门I
    题目传送门II

    一、差分约束

    这题很有意思,看了下题,感觉得用前缀和,然后似乎就是:
    我们用\(sum[i]\)表示前\(i\)个月的总利润
    如果从第\(x\)月到第\(y\)月的利润是\(z\)\(z\)可正可负)
    可以得到:\(sum[y] - sum[x-1] = z\)(可以理解为前缀和差值)
    相当于我们会得到很多这样的式子,然后去验证是否合法
    我们都知道差分约束是小于等于的关系,那这个题怎么做?

    sum[y] - sum[x-1] = z
    

    我们可以认为是同时满足下面两个式子:

    sum[y] - sum[x-1] > =z
    sum[y] - sum[x-1] < =z
    

    第一个式子可以转变成<=的形式:

    sum[x-1] < = sum[y]-z
    

    第二个式子可以转变成<=的形式:

    sum[y]  < = sum[x-1]+z
    

    这样就变成了差分约束的问题
    连边\((y,x-1)\),边权为\(-z\)
    连边\((x-1,y)\),边权为\(z\)
    然后跑最短路去验证是否有负环…(剩下的都是差分约束的过程了)

    #include <bits/stdc++.h>
    using namespace std;
    //这里的M取值需要总结一下,因为有100组以下的输入,所以还需要乘以100,才是真正的边数上限
    const int N = 110, M = N * N * 100;
    //邻接表
    int e[M], h[N], idx, w[M], ne[M];
    void add(int a, int b, int c) {
        e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
    }
    
    int d[N];   //最短距离数组
    bool st[N]; //是不是进过队列
    int cnt[N];
    int n, m, T;
    
    bool spfa(int start) { //求最长路,所以判断正环
        queue<int> q;      //有时候换成栈判断环很快就能
        //差分约束从超级源点出发
        d[start] = 0;
        q.push(start);
        st[start] = true;
    
        while (q.size()) {
            int t = q.front();
            q.pop();
            st[t] = false;
            for (int i = h[t]; ~i; i = ne[i]) {
                int j = e[i];
                if (d[j] > d[t] + w[i]) { //求最短路
                    d[j] = d[t] + w[i];
                    cnt[j] = cnt[t] + 1;
                    //注意多加了超级源点到各各节点的边
                    if (cnt[j] >= n + 1) return false;
                    if (!st[j]) {
                        q.push(j);
                        st[j] = true;
                    }
                }
            }
        }
        return true;
    }
    
    int main() {
        cin >> T;
        while (T--) {
            cin >> n >> m;
            memset(h, -1, sizeof h);
            memset(d, 0x3f, sizeof(d));
            memset(cnt, 0, sizeof(cnt));
            memset(st, 0, sizeof(st));
    
            while (m--) {
                int a, b, c;
                cin >> a >> b >> c;
                add(b, a - 1, -c), add(a - 1, b, c);
            }
            //建立超级源点
            for (int i = 1; i <= n; i++) add(n + 1, i, 0);
            //整条路径中存在负环,意味着不等式组存在矛盾
            if (!spfa(n + 1))
                cout << "false" << endl;
            else
                cout << "true" << endl;
        }
        return 0;
    }
    

    二、并查集

    处理是否存在冲突、矛盾的题,有时也可以采用并查集。

    维护到祖宗节点距离的并查集
    对于本题,我们使用维护到祖宗节点距离的并查集。其基础模板如下:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
    
    // 返回x的祖宗节点
    int find(int x) {
        if (p[x] == x) return x; 
        int root = find(p[x]);   
        d[x] += d[p[x]];         
        return p[x] = root;
    }
    
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
        p[i] = i, d[i] = 0;
    
    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
    

    那么,对于本体来说最为困难的是:如何确定合并两个集合时更新到祖宗节点距离的更新方式。我们不妨先来看这样一个例子,现有结点\(1,2,3,4\),有\(p[1]=3,p[2]=4,d[1]=10,d[2]=30\),即如下图所示:

    现我们要添加一条边\(1→2\)(边长\(w\)),即\(p[find(1)]=find(2)⇒p[3]=4\)。根据图示可以很容易得出\(d[3]=w(3→1)+w(1→2)+w(2→4)=−d[1]+w+d[2]\)。由此我们可以推断出,如果要添加一条边\(x→y(x<y)\) (将\(x\)合并到\(y\)),距离更新方式如下\(d[find(x)]=−d[x]+w+d[y]\)。如果要添加的这条边的两个端点已经位于同一个集合中,则直接判断\(d[x]−d[y](x<y)\)是否与\(w\)相等即可,如果不相等则可判定当前账本存在问题。

    #include <bits/stdc++.h>
    using namespace std;
    
    const int N = 10010;
    bool flag;
    int d[N]; //到根的距离
    int p[N]; //并查集数组
    int n, m;
    //带权并查集模板
    int find(int x) {
        //点权的理解:每个点到根的距离
        if (p[x] == x) return x; //自己是老大,返回自己
        int root = find(p[x]);   //通过自己父亲找老大
        d[x] += d[p[x]];         //点权在路径压缩过程中,需要加上父亲的点权
        return p[x] = root;      //路径压缩
    }
    
    //合并时进行d[px]修改是关键
    void join(int x, int y, int w) {
        int px = find(x), py = find(y);
        if (px != py) {
            p[px] = py;              //加入py家族
            d[px] = d[y] - d[x] + w; //更新px的距离
        } else if (d[x] - d[y] != w)
            flag = true;
    }
    
    int main() {
        int T;
        cin >> T;
        while (T--) {
            //多组测试数据,需要清空
            flag = false;
            memset(d, 0, sizeof(d)); //初始化每个节点到根的距离
    
            cin >> n >> m;
            //初始化并查集,注意此处从0开始!原因是前缀和从0开始,比如1-3月,那么其实是加入了0号家族
            for (int i = 0; i <= n; i++) p[i] = i, d[i] = 0;
            // m组条件
            while (m--) {
                int s, t, v; //开始月份,结束月份,区间内的
                cin >> s >> t >> v;
                //利用前缀和思想,检查从s到t这个区间内收益是不是v
                join(s - 1, t, v);
            }
            //有冲突
            if (flag)
                puts("false");
            else //无冲突
                puts("true");
        }
        return 0;
    }
    
  • 相关阅读:
    把arguments转化成数组
    最小化重绘和重排
    选择器API
    事件委托
    WAhaha_hnu (zoj 2010 oct月赛)
    素数计数公式全面拉丁化改写小有改进Meissel公式梅塞尔Lehmer公式莱梅=勒梅尔筛法三种形式孟庆余公式(转载)
    NBUT 2013 Timed NOJ Training #005
    2013 腾讯马拉松初赛第一场
    hrboj 1683 树形DP
    哈尔滨2013校赛训练赛 4 解题思路
  • 原文地址:https://www.cnblogs.com/littlehb/p/16128793.html
Copyright © 2020-2023  润新知