差分约束系统例题详解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
差分约束系统例题详解
例1:pku1364
已知一个序列a[1], a[2], ......, a[n],给出它的若干子序列以及对该子序列的约束条件,例如a[si], a[si+1], a[si+2], ......, a[si+ni],且a[si]+a[si+1]+a[si+2]+......+a[si+ni] < or > ki。
问题关键在于如何转化约束条件,开始我想以序列中的每一个值做一个点,如a[1], a[2] ……,就发现他们的关系是多者之间的关系,跟差分系统对不上,在看到MickJack的讲解后,才知道,用前n项和来转化成两两之间的关系。
如:s[a] + s[a+1] + …… + s[b] < c 可以转化成前n项和sum[b] - sum[a - 1] < c,为了能用Bellman_Ford,即将< 转化成<= ,sum[b] - sum[a - 1] <= c - 1。
我用Bellman_Ford写的:
#include<stdio.h>
#define INF 0xfffffff
#define NN 104
int index, n;
int dis[NN];
struct node{
int s, e, v;
}edge[NN];
void add(int a, int b, int c){
edge[index].s = a;
edge[index].e = b;
edge[index].v = c;
index++;
}
void Init(int s){
int i;
for (i = 0; i <= n; i++){
dis[i] = INF;
}
dis[s] = 0;
}
void Relax(int s, int e, int v){
if (dis[e] > dis[s] + v){
dis[e] = dis[s] + v;
}
}
/*查找负边权回路,1表示存在*/
int Bellman_Ford(){
Init(0);
int i, j;
for (i = 1; i <= n; i++){
for (j = 0; j < index; j++){
Relax(edge[j].s, edge[j].e, edge[j].v);
}
}
for (j = 0; j < index; j++){
if (dis[edge[j].e] > dis[edge[j].s] + edge[j].v){
return 1;
}
}
return 0;
}
int main()
{
char str[4];
int a, b, c, m;
while (scanf("%d", &n) != EOF){
if (n == 0) break;
scanf("%d", &m);
index = 0;
while (m--){
scanf("%d%d%s%d", &a, &b, str, &c);
if (str[0] == 'l'){
add(a - 1, a + b, c - 1); // c-1 使得< 变成<= 就能够判负环了
}else{
add(a + b, a - 1, -c - 1);
}
}
if(Bellman_Ford()) puts("successful conspiracy");
else puts("lamentable kingdom");
}
return 0;
}
做这题时,我用重新学习了下Bellman_Ford,感觉这个写的挺不错的。
为了锻炼一下,我又写了个SPFA,错了好次才过。
差分系统跟最短路还是有点区别的,就是得附加一个源点,使得他与所有点都相连,边权赋为零,这样才能保证连通,这一题我就错这个地方了。
开始写Bellman_Ford的时候没注意,没有加点也过了,后来发现SPFA就过不了,想了想,还是有道理的,bellman是对边操作,不加源点,照样能对任意一条边松弛,而SPFA就不一样,是对点操作,通过点,对与之相连的边进行松弛,就必须得附加源点了,不过有种方法可以不加,SPFA时,将所有点先入队,或将邻接表的根节点入队。
总结了以下关键点。
key1:做SPFA时,最好用邻接表存储,才能达到效果。
key2:为了不增加源点,将所有邻接表的根节点入队。
key3:为了减少队列队内存的负担,用循环队列实现,手动模拟更快,以前都是每向下一层,更新一次队列,现在是用循环,当队列满时更新队列,队列长度要大于队列一次可能保存的最多节点,这里就是节点的总个数,最多n+1个点同时都在队里,因为有mark 数组的限制,这里队列长度我取了n+2。
key4:当任意一节点入队次数超过|V|次的时候,即可判断有负权回路。
#include<stdio.h>
#include<string.h>
#define INF 0xfffffff
#define NN 106
int index, n;
int dis[NN]; /* 保存到源点距离,在这里没有设源,没有含义*/
int root[NN]; /* root[i] 找到与i节点相连的第一条后继边*/
int mark[NN]; /* 标记是否已入队*/
int next[NN]; /* 邻接的下一个定点*/
int cnt[NN]; /* 保存节点入队次数*/
struct node{
int e, v;
}edge[NN];
void add(int a, int b, int c){
int head, tmp;
edge[index].e = b;
edge[index].v = c;
head = root[a];
//key1
if (head == -1){
root[a] = index;
}else{
tmp = head;
while (next[tmp] != -1){
tmp = next[tmp];
}
next[tmp] = index;
}
next[index] = -1;
index++;
}
void Init(){
int i;
for (i = 0; i <= n; i++){
dis[i] = INF;
}
}
/*查找负边权回路,1表示存在*/
int Spfa(){
int i, len, cur, tmp, head, nxt;
int que[NN];
Init();
len = 0;
//key2
for (i = 0; i <= n; i++){
if (root[i] != -1){
que[len++] = i;
cnt[i] = 1;
mark[i] = 1;
}
}
//key3
for (i = 0; i != len; i = (i + 1) % (n + 2)){
cur = que[i];
head = root[cur];
tmp = head;
while (tmp != -1){
nxt = edge[tmp].e;
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
mark[nxt] = 1;
cnt[nxt]++;
que[len] = nxt;
len = (len + 1) % (n + 2);
//key4
if (cnt[nxt] > n + 1) return 1;
}
}
tmp = next[tmp];
}
mark[cur] = 0;
}
return 0;
}
int main()
{
char str[4];
int a, b, c, m;
while (scanf("%d", &n) != EOF){
if (n == 0) break;
scanf("%d", &m);
index = 0;
memset(root, -1, sizeof(root));
while (m--){
scanf("%d%d%s%d", &a, &b, str, &c);
if (str[0] == 'l'){
add(a - 1, a + b, c - 1);
}else{
add(a + b, a - 1, -c - 1);
}
}
memset(cnt, 0, sizeof(cnt));
memset(mark, 0, sizeof(mark));
if(Spfa()) puts("successful conspiracy");
else puts("lamentable kingdom");
}
return 0;
}
例2:pku3159(SPFA + 栈)
小孩A认为小孩B比自己多出的最多不会超过c个糖果,也就是B - A <= c,正好符合差分约束方程,就是A到B的边权w(A, B) = c;用SPFA + 栈能过。
这里有两种加边方式:
第一种:我以前用的,用这个超时了,因为每次加边都是将边夹在邻接表的最后面,需要一个查找时间,这题数据量大,自然就超时了。
void add(int a, int b, int c){
int tmp;
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = -1;
tmp = root[a];
if (tmp == -1){
root[a] = edNum;
}else{
while (next[tmp] != -1){
tmp = next[tmp];
}
next[tmp] = edNum;
}
edNum++;
}
第二种:这种我刚学到的,比较好,每次把边加在最前面,突然想起sjr有一道题的加边方法和这个一样,那时怎么就看不懂,大概是不懂邻接表的缘故吧。
void add(int a, int b, int c){
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = root[a];
root[a] = edNum++;
}
这题还得用栈实现,用队列超时,我开始用队列,一直TLE,我这里写了两个,Spfa()是用队列实现的,Spfa1是用栈实现的。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define INF 0xfffffff
#define MM 150004
#define NN 30004
int edNum, N;
struct node{
int e, v;
}edge[MM];
int next[MM];
int dis[NN];
int root[NN];
int que[NN];
int mark[NN];
int stack[NN];
void add(int a, int b, int c){
int tmp;
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = root[a];
root[a] = edNum++;
}
void Spfa()
{
int i, quNum, tmp, nxt, cur;
for (i = 1; i <= N; i++){
dis[i] = INF;
}
dis[1] = 0;
quNum = 0;
for (i = 1; i <= N; i++){
if (root[i] != -1){
que[quNum++] = i;
mark[i] = 1;
}
}
for (i = 0; i != quNum; i = (i + 1) % (N + 1)){
cur = que[i];
tmp = root[cur];
while (tmp != -1){
nxt = edge[tmp].e;
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
mark[nxt] = 1;
que[quNum] = nxt;
quNum = (quNum + 1) % (N + 1);
}
}
tmp = next[tmp];
}
mark[cur] = 0;
}
}
void Spfa1()
{
int i, top, tmp, nxt, cur;
for (i = 1; i <= N; i++){
dis[i] = INF;
}
dis[1] = 0;
top = 0;
for (i = 1; i <= N; i++){
if (root[i] != -1){
stack[++top] = i;
mark[i] = 1;
}
}
while (top){
cur = stack[top--];
tmp = root[cur];
mark[cur] = 0;
while (tmp != -1){
nxt = edge[tmp].e;
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
mark[nxt] = 1;
stack[++top] = nxt;
}
}
tmp = next[tmp];
}
}
}
int main()
{
int M, a, b, c, i;
scanf("%d%d", &N, &M);
edNum = 0;
for (i = 0; i <= N; i++){
root[i] = -1;
mark[i] = 0;
}
while (M--){
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
Spfa1();
printf("%d\n", dis[N]);
//system("pause");
return 0;
}
例3:pku2983(SPFA + 栈)
分析题意:
输入有两种形式:
1:P A B X 即B + X = A 转化一下:B - A <= -X, A - B <= X 构造边和权:(A, B, -X),(B, A, X)
2:V A B 即 B +1<= A 转化一下:B - A <= -1 构造边和权:(A, B, -1) WA了无数次,终于过了,不是SPFA的原因,是自己没写好啊!!每次写Bellman_Ford 的时候挺简单,写SPFA的时候就死活不过,一度让我怀疑,SPFA是不是不稳定!!不过还都是因为自己不过小心,处理的时候很容易出错。
不过SPFA还是很快的,422ms,Bellman_Ford就跑了2907ms,跟没过也差不多了!
#include<stdio.h>
#include<stdlib.h>
#define INF 0xfffffff
#define NN 1004
#define MM 200004
int edNum, N;
int next[MM];
int root[NN];
int mark[NN];
int cnt[NN];
int dis[NN];
int stack[NN];
struct node{
int e, v;
}edge[MM];
void add(int a, int b, int c){
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = root[a];
root[a] = edNum++;
}
int Spfa()
{
int i, top, cur, tmp, nxt;
top = 0;
//初始化的时候没有加源点,直接将所有有根的点入栈,也很不错for (i = 1; i <= N; i++){
dis[i] = INF;
// key1
if (root[i] != -1){
stack[++top] = i;
cnt[i]++;
mark[i] = 1;
}
}
while (top){
cur = stack[top--];
tmp = root[cur];
mark[cur] = 0;
while (tmp != -1){
nxt = edge[tmp].e;
//key2
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
cnt[nxt]++;
if (cnt[nxt] > N + 1){
return 1;
}
mark[nxt] = 1;
stack[++top] = nxt;
}
}
tmp = next[tmp];
}
}
return 0;
}
int main()
{
int i, a, b, c, M;
char str[2];
while (scanf("%d%d", &N, &M) != EOF)
{
edNum = 0;
for (i = 0; i <= N; i++){
root[i] = -1;
mark[i] = 0;
cnt[i] = 0;
}
while (M--){
scanf("%s%d%d", str, &a, &b);
if (str[0] == 'P'){
scanf("%d", &c);
add(b, a, c);
add(a, b, -c);
}else{
add(a, b, -1);
}
}
if (Spfa()){
puts("Unreliable");
}else{
puts("Reliable");
}
}
return 0;
}
例4:pku3169
这题还是用的SPFA + 栈过的,400多ms,写得多了,发现都可以套用模块了,除了输入不太一样外,其他的都基本一样。
模块一:负责各个数组的初始化
模块二:负责加边,对边用邻接表处理
模块三:SPFA 用栈来实现的过程
#include<stdio.h>
#include<stdlib.h>
#define INF 0xfffffff
#define NN 1004
#define MM 20004
int edNum, N;
int next[MM];
int root[NN];
int mark[NN];
int cnt[NN];
int dis[NN];
int stack[NN];
struct node{
int e, v;
}edge[MM];
/*在这里我用了一个函数来完成初始化,也可以看着一个固定模块了*/
/*模块一*/
void Init(){
int i;
for (i = 0; i <= N; i++){
root[i] = -1;
mark[i] = 0;
cnt[i] = 0;
}
}
/*模块二*/
void add(int a, int b, int c){
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = root[a];
root[a] = edNum++;
}
/*函数返回-1,表示有负边权回路,就表示不可能
函数返回-2,就表示起点到终点的距离不定,这里用dis[N] == INF,来判断,因为开始就是赋值的是INF
否者,返回dis[N],这里面存的就是起点到终点的最大距离
*/
/*模块三*/
int Spfa()
{
int i, top, cur, tmp, nxt;
top = 0;
for (i = 1; i <= N; i++){
dis[i] = INF;
if (root[i] != -1){
stack[++top] = i;
cnt[i]++;
mark[i] = 1;
}
}
dis[1] = 0;
while (top){
cur = stack[top--];
tmp = root[cur];
mark[cur] = 0;
while (tmp != -1){
nxt = edge[tmp].e;
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
cnt[nxt]++;
if (cnt[nxt] > N + 1){
return -1;
}
mark[nxt] = 1;
stack[++top] = nxt;
}
}
tmp = next[tmp];
}
}
if (dis[N] == INF){
return -2;
}
return dis[N];
}
int main()
{
int ML, MD, a, b, c;
scanf("%d%d%d", &N, &ML, &MD);
Init();
while (ML--){
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
while (MD--){
scanf("%d%d%d", &a, &b, &c);
add(b, a, -c);
}
printf("%d\n", Spfa());
//system("pause");
return 0;
}
例5:pku1201
这题依然SPFA + 栈过!
不同的是,这题有隐含条件,而且所求也大不一样。
由于这是对区间操作,求至少包含区间[ai, bi]中ci个点的最小集合。
可以用集合元素个数来定义变量,即num[bi] - num[ai] >= ci, 但有个隐含条件就是,每个元素都是整点,取得话,最多为一,最少为0, 即num[i+1] - num[i] <= 1, num[i+1] - num[i] >= 0, 将这些条件再转换成最短路里的边和权,就可以做了。
这里求得是至少,也就是最小值,即num[maxn] - num[0] >= ans, 我们要转化成<= 号,也就是num[0] - num[maxn] <= -ans, 这样就行了,赋初值时,令dis[maxn] = 0,求得就是每个点到maxn的距离了,输出结果就是dis[0]加个负号。
#include<stdio.h>
#include<stdlib.h>
#define INF 0xfffffff
#define NN 50004
#define MM 150004
int edNum, maxn;
int mark[NN];
int root[NN];
int dis[NN];
int next[MM];
int stack[NN];
struct node{
int e, v;
}edge[MM];
void Init(){
int i;
for (i = 0; i < NN; i++){
mark[i] = 0;
root[i] = -1;
dis[i] = INF;
}
}
void add(int a, int b, int c){
edge[edNum].e = b;
edge[edNum].v = c;
next[edNum] = root[a];
root[a] = edNum++;
}
void Spfa()
{
int i, cur, nxt, tmp, top = 0;
for (i = 0; i <= maxn; i++){
if (root[i] != -1){
stack[++top] = i;
mark[i] = 1;
}
}
// 这里需要注意,要将终点置零,求的就是别的点到终点maxn的距离dis[maxn] = 0;
while (top){
cur = stack[top--];
mark[cur] = 0;
tmp = root[cur];
while (tmp != -1){
nxt = edge[tmp].e;
if (dis[nxt] > dis[cur] + edge[tmp].v){
dis[nxt] = dis[cur] + edge[tmp].v;
if (!mark[nxt]){
mark[nxt] = 1;
stack[++top] = nxt;
}
}
tmp = next[tmp];
}
}
}
int main()
{
int n, i, a, b, c;
scanf("%d", &n);
maxn = 0;
edNum = 0;
Init();
for (i = 1; i <= n; i++){
scanf("%d%d%d", &a, &b, &c);
add(b + 1, a, -c);
if (b + 1 > maxn){
maxn = b + 1;
}
}
for (i = 0; i <= maxn; i++){
add(i + 1, i, 0);
add(i, i + 1, 1);
}
Spfa();
//输出的时候也要注意,是起点到终点的距离的负值
printf("%d\n", -dis[0]);
// system("pause");
return 0;
}。