差分约束学习指南
前置芝士
求解差分约束系统,有 m条约束条件,每条都为形如 \(( x_a-x_b\geq c_k)\),\((x_a-x_b\leq c_k)\)或\(x_a=x_b\) 的形式,判断该差分约束系统有没有解。
题意 | 转化 | 连边 |
---|---|---|
\((x_a - x_b \geq c)\) | \((x_b - x_a \leq -c)\) | add(a, b, -c); |
\(( "x_a - x_b \leq c")\) | \((x_a - x_b \leq c)\) | add(b, a, c); |
\((x_a = x_b)\) | \((x_a - x_b \leq 0, \space x_b - x_a \leq 0)\) | add(b, a, 0), add(a, b, 0); |
差分约束
给出一组包含 \(m\) 个不等式,有 \(n\) 个未知数的形如:
的不等式组,求任意一组满足这个不等式组的解。
[input]
第一行为两个正整数 \(n,m\),代表未知数的数量和不等式的数量。
接下来 \(m\) 行,每行包含三个整数 \(c,c',y\),代表一个不等式 \(x_c-x_{c'}\leq y\)。
[output]
一行,\(n\) 个数,表示 \(x_1 , x_2 \cdots x_n\) 的一组可行解,如果有多组解,请输出任意一组,无解请输出 NO
。
[a.in]
3 3
1 2 3
2 3 -2
1 3 1
[a.out]
5 3 5
\(\begin{cases}x_1-x_2\leq 3 \\ x_2 - x_3 \leq -2 \\ x_1 - x_3 \leq 1 \end{cases}\)
一种可行的方法是 \(x_1 = 5, x_2 = 3, x_3 = 5\)。
\(\begin{cases}5-3 = 2\leq 3 \\ 3 - 5 = -2 \leq -2 \\ 5 - 5 = 0\leq 1 \end{cases}\)
[datas]
\(1\leq n,m \leq 5\times 10^3\),\(-10^4\leq y\leq 10^4\),\(1\leq c,c'\leq n\),\(c \neq c'\)
solved
我们观察下这些不等式,像在最短路里的三角不等式,于是,发现了这些性质:
我们设 dis 数组代表长度,跟最短路里的dis 概念一样。
所以,我们只要对于每个不等式 \(x_i−x_j≤y\),连一条从j 到i 长度为y 的边,跑一遍最短路,即可得到一组解。备注:我们设 cnt 数组来表示这个点记录了几次,如果这个图有负环,说明 \(cnt_i≥n\),则要输出无解。这就是差分约束算法的重要思想。
[寻找所有x<=0最大解]
因此对于这类不等式组的求解,我们可以将其抽象成一个有 n 个点的最短路问题,对于不等式\(x_i−x_j≤y\),建一条从j连向i 边权为y的单向边。
补充: 如果存在符号相反的不等式,如\(x_i−x_j≥y\),我们可以通过给两边乘−1 的方式使其变号,变为 \(x_j−x_i≤−y\)。
建完图后,为了每个点的可达性,我们要新建一个超级源点s=n+1 向每个点连出一条边权为 0的边。
const int N=5010;
int n,m,h[N],idx;
struct edge{
int v,ne,w;
}e[N<<1];
int dis[N],cnt[N];
bool vis[N];
queue<int> q;
void add(int u,int v,int w){
e[++idx].v=v;
e[idx].ne=h[u];
e[idx].w=w;
h[u]=idx;
}
bool spfa(int s){
dis[s]=0;
vis[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=0;
for(int i=h[u];i;i=e[i].ne){
int v=e[i].v;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
if(vis[v]==0){
cnt[v]++;
if(cnt[v]>=n+1) return false;//多加了一个超级源点,所以总共的点数是n+1,最多也就被每个点更新一次
vis[v]=1;
q.push(v);
}
}
}
}
return true;
}
void solve(){
cin>>n>>m;
int s=n+1;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
add(v,u,w);
}
for(int i=1;i<=n;i++){
add(s,i,0);
}
memset(dis,0x3f,sizeof(dis));
if(!spfa(s)){
cout<<"NO"<<endl;
return;
}
for(int i=1;i<=n;i++)
cout<<dis[i]<<" ";
cout<<endl;
}
[寻找所有x>=0的最小值]
const int N=5010;
struct edge{
int ne,v,w;
}e[N<<1];
int idx,h[N];
int cnt[N],dis[N];
bool vis[N];
queue<int> q;
int n,m;
void add(int u,int v,int w){
e[++idx]={h[u],v,w};
h[u]=idx;
}
bool spfa(int s){
q.push(s);
vis[s]=true;
// cnt[s]++;
dis[s]=0;
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=false;
for(int i=h[u];i;i=e[i].ne){
int v=e[i].v;
int w=e[i].w;
if(dis[v]<dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]){
q.push(v);
vis[v]=true;
if(++cnt[v]>=n+1) {return false;}
}
}
}
}
return true;
}
void solve(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
add(u,v,-w);
}
int s=n+1;
for(int i=1;i<=n;i++) add(s,i,0);
memset(dis,-1,sizeof(dis));
if(!spfa(s)){
cout<<"NO"<<endl;
}else{
for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
cout<<endl;
}
}
糖果
幼儿园里有 \(N\) 个小朋友,\(\text{lxhgww}\) 老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候,\(\text{lxhgww}\) 需要满足小朋友们的 \(K\) 个要求。幼儿园的糖果总是有限的,\(\text{lxhgww}\) 想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。
[input]
输入的第一行是两个整数 \(N\),\(K\)。接下来 \(K\) 行,表示这些点需要满足的关系,每行 \(3\) 个数字,\(X\),\(A\),\(B\)。
- 如果 \(X=1\), 表示第 \(A\) 个小朋友分到的糖果必须和第 \(B\) 个小朋友分到的糖果一样多;
- 如果 \(X=2\), 表示第 \(A\) 个小朋友分到的糖果必须少于第 \(B\) 个小朋友分到的糖果;
- 如果 \(X=3\), 表示第 \(A\) 个小朋友分到的糖果必须不少于第 \(B\) 个小朋友分到的糖果;
- 如果 \(X=4\), 表示第 \(A\) 个小朋友分到的糖果必须多于第 \(B\) 个小朋友分到的糖果;
- 如果 \(X=5\), 表示第 \(A\) 个小朋友分到的糖果必须不多于第 \(B\) 个小朋友分到的糖果;
[output]
输出一行,表示 \(\text{lxhgww}\) 老师至少需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出 \(-1\)。
[a.in]
5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1
[a.out]
11
[datas]
对于所有的数据,保证 \(K\leq100000, 1\leq X\leq5, 1\leq A, B\leq N\)
solved
我们将小朋友当做点,小朋友之间限制条件当做边。
对于X=1、X=3、X=5时,我们不难知道,想要使用最少的糖果数量,那么最好的方式就是两个人糖果数量一样。
对于X=2,那么A=B-1最优。
对于X=4,那么B=A-1最优。
(1)建图
比如A要少于B,则建一条A->B的边,这样拓扑排序下来,可以保证处理每个点的糖果数量时,可以从小处理到大,符合上面的要求。我们就需要额外记录边权,X=1、3、5边权为0,X=2、4边权为1。X=1时需要建双向边!即A->B且B->A。
(2)判断无解
在建新图用来拓扑排序时,看两个点是否在同一个环内,在且边权为1则无解。
(3)更新糖果数量
dp[当前更新的点] = max(dp[当前更新的点],dp[当前删除的点] + nnei[当前删除的点][第j个邻居].边权)
(4)总体思路:建图(X=1:建边权为0的双向边,X=2、X=4建边权为1的单向边,X=3、X=5时建边权为0的单向边)——>Tarjan——>边建新图边判断无解——>用新图拓扑排序,并更新每个点所需要的糖果数量。
// A要少于B,则建一条A->B的边
const int N = 100010;
int n, k;
/*
scc:每个节点的归属新编号
cnt:缩点个数
low:最小可到达时间戳
dfn:每个节点的深度优先搜索时间戳
idx:时间
*/
int scc[N], cnt, low[N], dfn[N], idx, tot[N];
bool ins[N];
stack<int> s;
vector<node> e[N];
vector<node> newe[N];
struct node {
int ne;
int v;
};
int dp[N];
int in[N];
ll ans;
void tarjan(int u) {
low[u] = dfn[u] = ++idx;
ins[u] = true;
s.push(u);
int len = e[u].size();
for (int i = 0; i < len; i++) {
int v = e[u][i].ne;
if (dfn[v] == 0) {
tarjan(v);
low[u] = min(low[v], low[u]);
} else {
if (ins[v])
low[u] = min(low[u], low[v]);
//low[u]=min(low[u],dfn[v]);
}
}
if (dfn[u] == low[u]) {
cnt++;
scc[u] = cnt;
ins[u] = false;
tot[cnt]++;
while (s.top() != u) {
int t = s.top();
ins[t] = false;
scc[t] = cnt;
s.pop();
tot[cnt]++;
}
s.pop();
}
}
void solve() {
cin >> n >> k;
for (int i = 1; i <= k; i++) {
int t, x, y;
cin >> t >> x >> y;
switch (t) {
case 1: {
e[x].push_back((node) {y, 0});
e[y].push_back((node) {x, 0});
break;
}
case 2: {
e[x].push_back((node) {y, 1});
break;
}
case 3: {
e[y].push_back((node) {x, 0});
break;
}
case 4: {
e[y].push_back((node) {x, 1});
break;
}
case 5: {
e[x].push_back((node){y, 0});
break;
}
}
}
for(int i=1;i<=n;i++){
if(dfn[i]==0) tarjan(i);
}
for(int i=1;i<=n;i++){
int len=e[i].size();
for(int j=0;j<len;j++){
int v=e[i][j].ne;
int xx=scc[i];
int yy=scc[v];
if(xx==yy&&e[i][j].v==1){
cout<<-1<<endl;
return;
}
if(xx!=yy){
newe[xx].push_back((node){yy,e[i][j].v});
in[yy]++;
//会有重边
}
}
}
queue<int> q;
for(int i=1;i<=cnt;i++){
if(!in[i]){
q.push(i);
dp[i]=1;
}
while(!q.empty()){
int cur=q.front();
q.pop();
int len=newe[cur].size();
for(int i=0;i<len;i++){
int v=newe[cur][i].ne;
in[v]--;
dp[v]=max(dp[v],dp[cur]+newe[cur][i].v);
if(!in[v]) q.push(v);
}
}
}
for(int i=1;i<=cnt;i++){
ans+=(ll)dp[i]*tot[i];
}
cout<<ans<<endl;
}