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 
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 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 (将\(x\)合并到\(y\)),距离更新方式如下\(d[find(x)]=?d[x]+w+d[y]\)。如果要添加的这条边的两个端点已经位于同一个集合中,则直接判断\(d[x]?d[y](x是否与\(w\)相等即可,如果不相等则可判定当前账本存在问题。

#include 
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;
}