AcWing 2128. 狡猾的商人
一、差分约束
这题很有意思,看了下题,感觉得用前缀和,然后似乎就是:
我们用\(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;
}