图算法
|
差分约束系统的建立和求解 |
poj1201(1716类似),poj2983, poj3159 poj1275, poj1364 |
最小(大)费用最大流 |
poj2516, poj2195, poj3422(最大费用最大流) poj2135(很裸的最小费用最大流,不过有一个大坑。。。) |
|
双连通分量 |
poj2942,poj3694 |
|
强连通分支及其缩点 |
poj2186, poj3592, poj3114 |
|
图的割边和割点 |
poj3352(外加3117) |
|
最小割模型 |
poj3308, poj3155(偏难)详见:http://www.cnblogs.com/vongang/archive/2012/10/25/2740042.html |
|
KM算法(最大权/最小权) |
poj2195, poj2400, poj3686 |
差分约束系统的建立和求解
表示差分约束以前没接触过,看了看算导。貌似就是一个建模,剩下的就是spfa,或则Bellman-Ford,学习了一下stack实现的spfa,据说这个效率高些;
poj 1201
建图:可以得到约束条件 num[b+1] - num[a] >= c;还有一个条件,0 <= num[i+1] - num[i] <= 1;还是理解的不深,多做练习!
poj 2983
题意:一个点集,给出M描述。P, A, B, C,表示a, b的距离确定为c,V,A,B表示a, b的距离至少为1。求能否同时满足这M个描述。
思路:
建图:b - a = c ----> (b - a >= c) && (b - a <= c) ----> (a - b <= -c) && (b - a <= c);
b - a >= 1 ----> (a - b <= -1)
加一个超级源点。题目要求同时满足M个描述,表示这个图是正常图(不含负权回路)。用spfa解,判断是否有负权回路就可以。
手写的queue,注意别开太小了。RE了两次!

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
using namespace std;
const int M = 1024;
const int inf = ~0u>>1;
struct node {
int to;
int val;
int next;
} g[2000007];
int head[M], t, n;
int q[10000000];
int dis[M];
int cnt[M];
bool vis[M];
void init() {
for(int i = 0; i < M; ++i) {
head[i] = 0; dis[i] = inf;
cnt[i] = 0; vis[i] = false;
}
t = 1;
}
void add(int u, int v, int c) {
g[t].to = v; g[t].val = c;
g[t].next = head[u]; head[u] = t++;
}
bool spfa() {
int i, u, v, val, f = 0, r = 0;
q[r++] = 0; vis[0] = true;
dis[0] = 0; //cnt[0] = 1;
while(f < r) {
u = q[f++];
vis[u] = false;
for(i = head[u]; i; i = g[i].next) {
v = g[i].to; val = g[i].val;
if(dis[v] > dis[u] + val) {
dis[v] = dis[u] + val;
if(!vis[v]) {
vis[v] = true;
q[r++] = v;
if(++cnt[v] > n) return false;
}
}
}
}
return true;
}
int main() {
//freopen("data.in", "r", stdin);
int m, i, a, b, c;
char s[2];
while(~scanf("%d%d", &n, &m)) {
init();
while(m--) {
scanf("%s", s);
if(s[0] == 'P') {
scanf("%d%d%d", &a, &b, &c);
add(b, a, c);
add(a, b, -c);
} else if(s[0] == 'V') {
scanf("%d%d", &a, &b);
add(a, b, -1);
}
}
for(i = 1; i <= n; ++i) {
add(0, i, 0);
}
if(spfa()) puts("Reliable");
else puts("Unreliable");
}
return 0;
}
poj 3159
题意:flymouse是班长,老师给了一些糖果让他分配。小孩们爱攀比,相互比较糖果数,A小孩认为B小孩的糖果数不能比他的多c个。此外,还有一个小孩snoopy专门监督班长flymouse,现在flymouse想整他,使他所得到的糖果数比snoopy的糖果数尽量多(在让每一个小孩都满意的情况下。)求这个最大数。
思路:dis[A]表示A小孩的糖果数,dis[B]表示B小孩的糖果数,满足dis[B] - dis[A] <= c 即 dis[B] <= dis[A] + c。也就是说当dis[B] > dis[A] + c时,进行松弛。这样就可以在AB上建一条权值为c的边,求dis[snoopy]到dis[flymouse]的最短路。spfa();
ps:我发现受1201的影响,2983和3159这两道题的思路都被局限住了。。。应该是我理解的不够T_T
poj 1275
黑书上的原题,首先将数据整理成:
r[1...24]---每个小时需要的出纳员的数目;
num[1...24]---每个小时应征的申请者的数目;
求s[1...24]----s[i]表示0~i时刻雇佣的出纳员的总数。s[i] - s[i-1]就是在i时刻录用的出纳员的数目。设s[0] = 0,sum为雇佣的所有出纳员的数目。于是可行的方案满足:
s[j] - s[j - 1] >= 0;
s[i-1] - s[i] >= -t[i];
s[24] - s[0] >= sum;
i = (j + 8 - 1)%24 + 1;
s[i] - s[j] >= r[i] (i > j);
s[i] - s[j] >= r[i] - sum (i > j);
当sum已知的时候,根据这些不等式构图:以0,1,...,24为顶点,如果存在不等式s[a] - s[b] >= c则add(b, a, c)。以0为定点用spfa求出dis[24],如果dis[24] = sum则这是一个可行解。
ps:莫名奇妙的debug了两个小时,样例就是不过,后来重新敲了一遍过了。。。无语!

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
const int N = 1024;
const int M = 30;
const int inf = ~0u>>2;
struct node {
int to;
int val;
int next;
} g[N];
int head[M], t;
int num[M], r[M];
int dis[M];
int cnt[M];
bool vis[M];
void init() {
for(int i = 0; i < M; ++i) {
head[i] = 0; vis[i] = false;
dis[i] = -inf;
}
t = 1;
}
void add(int u, int v, int w) {
g[t].to = v; g[t].val = w;
g[t].next = head[u]; head[u] = t++;
}
int spfa() {
int i, u, v, w;
queue<int> q;
for(i = 0; i < M; ++i) cnt[i] = 0;
q.push(0); dis[0] = 0; vis[0] = true; cnt[0] ++;
while(!q.empty()) {
u = q.front(); q.pop();
vis[u] = false;
for(i = head[u]; i; i = g[i].next) {
v = g[i].to; w = g[i].val;
if(dis[v] < dis[u] + w) {
dis[v] = dis[u] + w;
if(!vis[v]) {
vis[v] = true;
q.push(v);
if(++cnt[v] > 24) return -1;
}
}
}
}
return dis[24];
}
int main() {
//freopen("data.in", "r", stdin);
int cas, n, i, j;
int x, sum;
scanf("%d", &cas);
while(cas--) {
memset(num, 0, sizeof(num));
for(i = 1; i <= 24; ++i) {
scanf("%d", &r[i]);
}
scanf("%d", &n);
for(i = 0; i < n; ++i) {
scanf("%d", &x);
num[x+1]++;
}
bool flag = false;
for(sum = 0; sum <= n; ++sum) {
init();
for(i = 1; i <= 24; ++i) {
add(i - 1, i, 0);
add(i, i - 1, -num[i]);
}
add(0, 24, sum);
for(j = 1; j <= 24; ++j) {
i = (j + 7)%24 + 1;
if(i > j) add(j, i, r[i]);
else add(j, i, r[i] - sum);
}
if(spfa() == sum) {
printf("%d\n", sum);
flag = true; break;
}
}
if(!flag) puts("No Solution");
}
return 0;
}
poj 1364
题意:国王有一个傻儿子,只会算一个序列S = {a1, a2, ..., an}里的子序列Si = {aSi, aSi+1, ..., aSi+ni}的和sum,并且可以将sum和给定的数ki进行比较,判断大小(sum > ki or sum < ki)。有时候他的判断是错的,但是不想承认,所以要伪造一个序列S,让这个伪造的序列满足他的所有判断都正确。问这个序列是否存在。
这个题数据给的很巧,都是整数,要不然没办法做了!设sum[i]表示sum(a0,...ai)。所以:
a b gt c => sum[b + a] - sum[a] + 1 > c;
a b lt c => sum[b + a] - sum[a] + 1 < c;
转化为:
sum[b + a] - sum[a] + 1 >= c + 1;
sum[b + 1] - sum[a] + 1 <= c - 1;
再转化成求最长路:
sum[b + a] - sum[a] + 1 >= c + 1; => add(b + a + 1, a, c + 1);
sum[a] - (sum[b + a] + 1) >= -(c - 1); => add(a, b + a + 1, 1 - c);
最后spfa判断是否有负权回路(也就是这个图存不存在),感谢wy教主指点。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 110;
const int inf = ~0u>>2;
struct node {
int to;
int val;
int next;
} g[N*N*N];
int head[N], t;
int dis[N];
int cnt[N];
bool vis[N];
int n, m;
void init() {
for(int i = 0; i < N; ++i) {
head[i] = 0; vis[i] = false;
cnt[i] = 0; dis[i] = -inf;
}
t = 1;
}
void add(int u, int v, int w) {
g[t].to = v; g[t].val = w;
g[t].next = head[u]; head[u] = t++;
}
bool spfa() {
int u, v, w, i;
queue<int> q;
q.push(0); dis[0] = 0;
vis[0] = true;
while(!q.empty()) {
u = q.front(); q.pop();
for(i = head[u]; i; i = g[i].next) {
v = g[i].to; w = g[i].val;
if(dis[v] < dis[u] + w) {
dis[v] = dis[u] + w;
if(!vis[v]) {
vis[v] = true;
q.push(v);
if(++cnt[v] > n) return false;
}
}
}
vis[u] = false;
}
return true;
}
int main() {
//freopen("data.in", "r", stdin);
int i, a, b, c;
char s[10];
while(scanf("%d", &n), n) {
scanf("%d", &m);
init();
for(i = 0; i < m; ++i) {
scanf("%d %d %s %d", &a, &b, s, &c);
b += a;
if(s[0] == 'g') add(b + 1, a, c + 1);
else add(a, b + 1, 1 - c);
}
++n;
for(i = 1; i <= n; ++i) {
add(0, i, 0);
}
if(spfa()) puts("lamentable kingdom");
else puts("successful conspiracy");
}
return 0;
}
最小(大)费用最大流
poj 2516
题意:描述的很让人费解,多读几遍就可以了。N个收货地,每个收货要K种物品,每种物品ki(0 <= ki <= 3)个。M个货源地,每个货源地同样K种物品,各种物品ki'(0 <= ki' <= 3)个。然后是K个N*M的矩阵,第ki‘’个矩阵的第i 行第j 列表示从货源地j 到收货地i 运送ki‘’型号的物品的单价。(无比蛋疼的表述,看得我想哭的心都有!!)
思路:很裸的最小费用最大流,把K种物品分别计算出最小费用最大流,然后求和就可以。
最小费用最大流详见:

#include <iostream> #include <cstring> #include <cstdio> #include <queue> using namespace std; const int N = 110; const int inf = ~0u>>2; int c[N][N], f[N][N], w[N][N]; int dis[N]; int pre[N]; bool vis[N]; int s, t; int ord[N][N]; int sto[N][N]; int cos[N][N][N]; void spfa() { int u, v; queue<int> q; q.push(s); dis[s] = 0; vis[s] = true; while(!q.empty()) { u = q.front(); q.pop(); for(v = 0; v <= t; ++v) { if(c[u][v] > f[u][v] && dis[v] > dis[u] + w[u][v]) { dis[v] = dis[u] + w[u][v]; pre[v] = u; if(!vis[v]) { vis[v] = true; q.push(v); } } } vis[u] = false; } } void mcmf() { int i; while(1) { for(i = 0; i < N; ++i) { dis[i] = inf; pre[i] = -1; vis[i] = false; } spfa(); if(pre[t] == -1) break; int x = t, minf = inf; while(pre[x] != -1) { minf = min(minf, c[pre[x]][x] - f[pre[x]][x]); x = pre[x]; } x = t; while(pre[x] != -1) { f[pre[x]][x] += minf; f[x][pre[x]] = -f[pre[x]][x]; x = pre[x]; } } } int main() { //freopen("data.in", "r", stdin); int n, m, k; int i, j, l; int ans, flag; while(~scanf("%d%d%d", &n, &m, &k)) { if(n + m + k <= 0) break; memset(ord, 0, sizeof(ord)); memset(sto, 0, sizeof(sto)); memset(cos, 0, sizeof(cos)); for(i = 1; i <= n; ++i) for(j = 1; j <= k; ++j) scanf("%d", &ord[i][j]); for(i = 1; i <= m; ++i) { for(j = 1; j <= k; ++j) { scanf("%d", &sto[i][j]); } } for(l = 1; l <= k; ++l) { for(i = 1; i <= n; ++i) { for(j = 1; j <= m; ++j) { scanf("%d", &cos[l][i][j]); } } } s = 0; t = m + n + 1; ans = 0; flag = 1; for(l = 1; l <= k && flag; ++l) { memset(w, 0, sizeof(w)); memset(f, 0, sizeof(f)); memset(c, 0, sizeof(c)); for(i = 1; i <= m; ++i) c[s][i] = sto[i][l]; for(i = 1; i <= n; ++i) c[i + m][t] = ord[i][l]; for(i = 1; i <= m; ++i) { for(j = 1; j <= n; ++j) { c[i][j + m] = sto[i][l]; } } for(i = 1; i <= m; ++i) { for(j = 1; j <= n; ++j) { w[i][j + m] = cos[l][j][i]; w[j + m][i] = -w[i][j + m]; } } mcmf(); for(i = 1; i <= n; ++i) { if(c[i + m][t] != f[i + m][t]) {flag = 0; break;} } for(i = 1; i <= m; ++i) { for(j = 1; j <= n; ++j) { ans += w[i][j + m] * f[i][j + m]; } } } if(!flag) puts("-1"); else printf("%d\n", ans); } return 0; }
poj 2195
题意:小人往house里走,给出图,m表示小人,H表示house。m每走一步要消费1¥,求让每个小人都进房间的最小消费。
思路:先求出每一小人到每个house的距离,dis(m, h) = abs(m.x - h.x) + abs(m.y - h.y);然后建图,i -> j(i ∈ set(m), j ∈ set(H))的容量为1 c(i, j),权值为 dis(i, j)。求出流量f(i, j)
ans = ∑[f(i, j) * dis(i, j));
ps: 把图建错了,wa来一下午。。。T_T'

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <queue> using namespace std; const int N = 210; const int inf = ~0u>>2; struct node { int x; int y; } stk[N*N]; int c[N][N], f[N][N], w[N][N]; int dis[N]; int pre[N]; bool vis[N]; int s, t; int n, m; char mp[N][N]; int ABS(int x) { return x < 0 ? -x : x; } void spfa() { int u, v; queue<int> q; q.push(s); dis[s] = 0; vis[s] = true; while(!q.empty()) { u = q.front(); q.pop(); for(v = 0; v <= t; ++v) { if(c[u][v] > f[u][v] && dis[v] > dis[u] + w[u][v]) { dis[v] = dis[u] + w[u][v]; pre[v] = u; if(!vis[v]) { vis[v] = true; q.push(v); } } } vis[u] = false; } } void mcmf() { int i, x, minf; while(1) { for(i = 0; i < N; ++i) { dis[i] = inf; vis[i] = false; pre[i] = -1; } spfa(); if(pre[t] == -1) break; minf = inf; x = t; while(pre[x] != -1) { minf = min(minf, c[pre[x]][x] - f[pre[x]][x]); x = pre[x]; } x = t; while(pre[x] != -1) { f[pre[x]][x] += minf; f[x][pre[x]] = -f[pre[x]][x]; x = pre[x]; } } } int main() { //freopen("data.in", "r", stdin); int i, j, num, hn, mn; int ans; while(scanf("%d%d", &n, &m), n||m) { num = 1; memset(mp, 0, sizeof(mp)); for(i = 0; i < n; ++i) { scanf("%s", mp[i]); for(j = 0; j < m; ++j) { if(mp[i][j] == 'm') { stk[num].x = i; stk[num++].y = j; } } } mn = num - 1; for(i = 0; i < n; ++i) { for(j = 0; j < m; ++j) { if(mp[i][j] == 'H') { stk[num].x = i; stk[num++].y = j; } } } hn = num - 1; memset(w, 0, sizeof(w)); memset(c, 0, sizeof(c)); for(i = 1; i <= mn; ++i) { for(j = mn + 1; j <= hn; ++j) { w[i][j] = ABS(stk[i].x - stk[j].x) + ABS(stk[i].y - stk[j].y); c[i][j] = 1; w[j][i] = -w[i][j]; } } s = 0; t = hn + 1; for(i = 1; i <= mn; ++i) { c[0][i] = 1; } for(i = mn + 1; i <= hn; ++i) { c[i][t] = 1; } memset(f, 0, sizeof(f)); mcmf(); ans = 0; for(i = 1; i <= mn; ++i) { for(j = mn + 1; j <= hn; ++j) { ans += w[i][j]*f[i][j]; } } printf("%d\n", ans); } return 0; }
poj 3422
题意:一个n*n的棋盘,每个格子里有一个非负整数,现在要从(1,1)点到(n,n)点走k次,每以小步只能向下或者向右,累加每小步走过的格子里边的数,然后将它置0。求最大累加值。
思路:最大费用最大流,据说是个经典问题,在群里问了一下,都建议我百度。好吧,以下内容是google的。
建图:把每个点拆成两个,A = (i - 1)*n + j,A‘ = A + n*n。
1、A->A'建一条容量为1,费用为map[i][j]的边,再建一条容量为inf,费用为0的边(这里是因为如果A点被一次增广路覆盖以后费用变成0,并且保证能无限次的再经过A点找增广路)。
2、与A相邻的点B(右或者下)。A‘ -> B建一条容量为inf,费用为0的边(同上,保证A’->B连通)。
3、最后从源点S到1建一条容量为k,费用为0的边,从2*n*n到汇点T建一条容量为k,费用为0的边。
然后spfa求增广路上的最长路,然后累加就可以。
ps:写邻接表一定memset(head, -1, sizeof(head)); 从0开始存,就因为这个昨晚TLE到1点半。。。想死的心都有!

#include <iostream> #include <cstring> #include <cstdio> #include <queue> using namespace std; const int N = 30000; const int M = 55; const int inf = ~0u>>2; struct node { int u, v; int cap; int cost; int next; } g[1000000]; int q[1000000]; int head[N], t; int dis[N]; int pre[N]; bool vis[N]; int mp[M][M]; int S, T; void add(int u, int v, int c, int f) { g[t].u = u; g[t].v = v; g[t].cap = c; g[t].cost = f; g[t].next = head[u]; head[u] = t++; g[t].u = v; g[t].v = u; g[t].cap = 0; g[t].cost = -f; g[t].next = head[v]; head[v] = t++; } bool spfa() { int u, v, i, f = 0, r = 0; for(i = S; i <= T; ++i) { dis[i] = -inf; vis[i] = false; pre[i] = -1; } q[r++] = S; vis[S] = true; dis[S] = 0; while(f < r) { u = q[f++]; vis[u] = false; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].v; if(g[i].cap > 0 && dis[v] < dis[u] + g[i].cost) { dis[v] = dis[u] + g[i].cost; pre[v] = i; if(!vis[v]) { vis[v] = true; q[r++] = v; } } } } if(pre[T] == -1) return false; return true; } int mcmf() { int minf, u, ans = 0; while(spfa()) { u = T; minf = inf; while(u != S) { minf = min(minf, g[pre[u]].cap); u = g[pre[u]].u; } u = T; while(u != S) { g[pre[u]].cap -= minf; g[pre[u]^1].cap += minf; u = g[pre[u]].u; } ans += dis[T]*minf; } return ans; } int main() { //freopen("data.in", "r", stdin); int i, j, n, k; int x, u, v; while(~scanf("%d%d", &n, &k)) { t = 0; memset(head, -1, sizeof(head)); S = 0; x = n*n; T = 2*n*n + 1; for(i = 1; i <= n; ++i) { for(j = 1; j <= n; ++j) { scanf("%d", &mp[i][j]); } } for(i = 1; i <= n; ++i) { for(j = 1; j <= n; ++j) { u = (i - 1)*n + j; v = u + x; add(u, v, 1, mp[i][j]); add(u, v, inf, 0); if(i < n) add(v, u + n, inf, 0); if(j < n) add(v, u + 1, inf, 0); } } add(S, 1, k, 0); add(2*x, T, k, 0); printf("%d\n", mcmf()); } return 0; }
双连通分量
poj 2942
题意:一群圆桌骑士闲的蛋疼一开会就要打架 ,国王老大看不下去了,找来军师梅林。梅林研究了一下说了两个规则,1、互相瞪眼的两个骑士不能挨着坐。2、一个圆桌上只能有奇数个骑士。
如果有的骑士因为这两个条件没地方坐,那就直接开除(没办法,人缘不行,^^)。给出哪些骑士互相瞪眼,算最少开除几个。
思路:建图时按照矛盾关系的补图建图,也就是说不矛盾的两个骑士连一条无向边。因为是无向图,所以先求无向图的点双连通分量。
1、得到的双连通分量,找到一个包含所有点的圈,如果这个圈是奇圈,那么这个双连通分量是合法的。如果这个圈是偶圈,假设在这个双连通分量中存在另外一个子圈是奇圈,那么其他的点一点在另外一个是奇圈的子圈内部。 所以:如果一个双连通分量存在一个奇圈,则这个双连通分量合法。
2、可以用二分图判是否存在奇圈。从二分图的概念知道,二分图一定是个偶圈(从一个节点U出发再回到U经过偶数条边),所以只要判断双连通分量不是二分图就可以确定它含有奇圈。用dfs对图进行01染色,如果存在前驱和后继节点同色,则可以确定不是二分图。
ps:对于这道题,已经不是做题了,而是啃题。。。

#include <iostream> #include <cstring> #include <cstdio> #include <queue> #define REP(i, n) for(i = 0; i < (n); ++i) #define FOR(i, l, h) for(i = (l); i <= (h); ++i) #define FORD(i, h, l) for(i = (h); i >= (l); --i) #define clear(x, i) memset(x, i, sizeof(x)); using namespace std; const int N = 1024; int ans[N][N]; int mp[N][N]; int dfn[N]; int low[N]; int col[N]; int cnt[N]; int stk[N]; int is[N]; int top, ind, num, n; void init() { clear(dfn, 0); clear(low, 0); clear(ans, 0); clear(cnt, 0); clear(stk, 0); clear(is, 0); top = ind = num = 0; } bool check(int n, int f, int i, int c) { int j; REP(j, cnt[n]) { if(mp[ans[n][i]][ans[n][j]] && j != f) { if(!col[j]) { col[j] = 3 - c; if(check(n, i, j, col[j])) return 1; } else if(col[i] == col[j]) return 1; } } return 0; } void tarjan(int u) { stk[top++] = u; dfn[u] = low[u] = ++ind; int v, i; FOR(v, 1, n) { if(mp[u][v]) { if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); if(dfn[u] <= low[v]) { ans[num][0] = u; stk[top] = 0; for(i = 1; stk[top] != v; ++i) { ans[num][i] = stk[--top]; } if(i > 2) cnt[num++] = i; } } else low[u] = min(low[u], dfn[v]); } } } int main() { //freopen("data.in", "r", stdin); int m, i, j, x, y, res; while(scanf("%d%d", &n, &m), n || m) { init(); REP(i, n + 1) REP(j, n + 1) mp[i][j] = (i == j ? 0 : 1); while(m--) { scanf("%d%d", &x, &y); mp[x][y] = mp[y][x] = 0; } FOR(i, 1, n) if(!dfn[i]) tarjan(i); REP(i, num) { clear(col, 0); col[0] = 1; if(check(i, -1, 0, 1)) { REP(j, cnt[i]) is[ans[i][j]] = 1; } } res = 0; FOR(i, 1, n) res += (is[i]^1); printf("%d\n", res); } return 0; }
poj 3694
题意:N个点M条无向边构成一个图,然后是Q个询问,每次加一条边,求每加一条边图中还剩的桥的个数。
思路:先用tarjan求出桥边,这里如果u->v是桥边的话记录v点就可以表示。在求桥边的过程中记录每个节点的父节点,然后求最近公共祖先。这里有两个处理:1、因为如果a->b加一条边的话,a,b,a和b的最近公共祖先就构成一个环,上边所有的桥边都要剪掉。2、a->b连上,a到b这条路上所有的桥也都删掉,同样b->a。
详见代码:

#include <iostream> #include <cstdio> #include <cstring> #include <string> #include <queue> #include <stack> #include <map> #include <vector> #include <algorithm> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, r) for((i) = (l); (i) <= (r); ++(i)) #define FORD(i, r, l) for((i) = (r); (i) >= (l); --(i)) #define CL(x, i) memset((x), (i), sizeof((x))) using namespace std; const int N = 100010; struct node { int to; int next; } g[N<<4]; int cnt, t, ind; int head[N]; int dfn[N]; int low[N]; int brg[N]; int far[N]; bool vis[N]; void init() { CL(vis, false); CL(dfn, 0); CL(low, 0); CL(brg, 0); CL(far, 0); CL(head, -1); cnt = ind = t = 0; } void add(int u, int v) { g[t].to = v; g[t].next = head[u]; head[u] = t++; } void dfs(int u) { //tarjan求桥 vis[u] = true; dfn[u] = low[u] = ++ind; int i, v; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(!dfn[v]) { far[v] = u; dfs(v); low[u] = min(low[u], low[v]); if(low[v] > dfn[u]) { ++cnt; brg[v] = 1; //记录桥边 } } else if(v != far[u] && vis[v]) { low[u] = min(low[u], dfn[v]); } } } void lca(int u, int v) { while(dfn[v] > dfn[u]) { //v到u路径上 if(brg[v]) { --cnt; brg[v] = 0; } v = far[v]; } while(dfn[u] > dfn[v]) { //u到v路径上 if(brg[u]) { --cnt; brg[u] = 0; } u = far[u]; } while(u != v) { //u,v,u和v的最近公共祖先所构成的环 if(brg[u]) { --cnt; brg[u] = 0; } if(brg[v]) { --cnt; brg[v] = 0; } v = far[v]; u = far[u]; } } int main() { //freopen("data.in", "r", stdin); int n, m, q, i; int cas = 0, a, b; while(~scanf("%d%d", &n, &m)) { if(n + m == 0) break; init(); for(i = 0; i < m; ++i) { scanf("%d%d", &a, &b); add(a, b); add(b, a); } dfs(1); scanf("%d", &q); printf("Case %d:\n", ++cas); while(q--) { scanf("%d%d", &a, &b); lca(a, b); printf("%d\n", cnt); } cout << endl; } return 0; }
强连通分支及其缩点
poj 2186
题意:如果AB有一条边,则表示A认为B是popular,如果AB,BC,则A同样认为C是popular。求被所有的点都认为是popular的点的个数。
思路:可以先缩点,得到一个新图,再按原图的边把个个缩点连接,找出度为0的缩点。此缩点的包含的所有点都是所求的点。

#include <iostream> #include <cstring> #include <cstdio> #include <cmath> #include <algorithm> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, h) for(i = l; i <= h; ++i) #define FORD(i, h, l) for(i = h; i >= l; --i) #define CL(x, n) memset(x, n, sizeof(x)) using namespace std; const int N = 10007; const int M = 50007; struct node { int to; int next; } g[M<<1]; int head[N]; int dfn[N]; int low[N]; int scc[N]; int val[N]; int stk[N]; int out[N]; bool vis[N]; int cnt, ind, top, t; void init() { CL(head, -1); CL(dfn, 0); CL(low, 0); CL(scc, 0); CL(val, 0); CL(stk, 0); CL(vis, 0); CL(out, 0); cnt = ind = top = t = 0; } void add(int u, int v) { g[t].to = v; g[t].next = head[u]; head[u] = t++; } void tarjan(int u) { vis[u] = true; dfn[u] = low[u] = ++ind; stk[top++] = u; int i, v; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(vis[v]) { low[u] = min(low[u], dfn[v]); } } if(low[u] == dfn[u]) { cnt ++; do { v = stk[--top]; scc[v] = cnt; val[cnt]++; } while(v != u); } } int main() { //freopen("data.in", "r", stdin); int n, m, a, b; int i, j, ou, f; while(~scanf("%d%d", &n, &m)) { init(); while(m--) { scanf("%d%d", &a, &b); add(a, b); } FOR(i, 1, n) { if(!dfn[i]) tarjan(i); } FOR(j, 1, n) { for(i = head[j]; i != -1; i = g[i].next) { a = g[i].to; if(scc[j] != scc[a]) { out[scc[j]]++; } } } ou = 0; FOR(i, 1, cnt) { if(!out[i]) {ou++; f = i;} } if(ou > 1) printf("0\n"); else printf("%d\n", val[f]); } return 0; }
poj 3529
题意:N*M的棋盘,每个格子里有一定数目的ores,从左上角开始走,每次只能向右或向下,每走过一个格子里边的矿石被取干净。途中可能遇到传送阵,每一传送阵有一个值(u, v)。表示可以把你传送到(u, v)处,当然你可以选择传送还是不传送。求最大可能得到的矿石数。
思路:很明显要tarjan缩点,然后重新建图。开始想的是建成的图必定是一颗树,所以用树形dp的思想dfs,可是wa掉N次后本菜打消了这个念头。后来看到AC大神blog里写过,用spfa求最长路。这也没什么难的嘛,最近一直在跟spfa打交道,三角形不等式快用烂了。。。可是。。。可是!!尼嘛从昨晚wa了整整一晚上,今天早晨起来一看,狗血的发现建图的时候把权值加错了。。。我都让我整整刷了一页的wa。。。今天早晨又拍了一遍,妹的又是因为一个很小的错误调了N久。。。做题不仔细,这就是后果。。。T_T
要注意的几点
1、in the order from north to south then west to east,这句坑死人不偿命的话。。
2、传送阵也可以下右走或者向下走。
3、重新建图时加的是缩点的权。。。在这里wa死了!T_T

#include <iostream> #include <cstring> #include <cstdio> #include <queue> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, h) for(i = l; i <= h; ++i) #define FORD(i, h, l) for(i = h; i >= l; --i) #define CL(x, i) memset(x, i, sizeof(x)) using namespace std; const int N = 2048; const int inf = ~0u>>2; struct node { int to; int next; } g[N*100]; struct node2 { int to; int val; int next; } G[N*100]; int head[N], t; int head2[N], t2; int dfn[N], low[N], stk[N*100]; int val[N], ore[N], scc[N]; int top, ind, cnt; bool inq[N]; int dis[N]; bool vis[N]; char mp[50][50]; int num[50][50]; void init() { CL(head, -1); CL(head2, -1); CL(vis, false); CL(inq, false); CL(dfn, 0); CL(low, 0); CL(stk, 0); CL(val, 0); CL(ore, 0); CL(num, 0); CL(mp, 0); CL(scc, 0); t = t2 = ind = top = cnt = 0; } void add(int u, int v) { g[t].to = v; g[t].next = head[u]; head[u] = t++; } void add2(int u, int v, int w) { G[t2].to = v; G[t2].val = w; G[t2].next = head2[u]; head2[u] = t2++; } void tarjan(int u) { inq[u] = true; dfn[u] = low[u] = ++ind; stk[top++] = u; int i, v; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(inq[v]) { low[u] = min(low[u], dfn[v]); } } if(low[u] == dfn[u]) { cnt ++; do { v = stk[--top]; scc[v] = cnt; inq[v] = false; val[cnt] += ore[v]; } while(u != v); } } int spfa() { int i, u, v, w; queue<int> q; REP(i, N) dis[i] = -inf; q.push(0); vis[0] = true; dis[0] = 0; while(!q.empty()) { u = q.front(); q.pop(); vis[u] = false; for(i = head2[u]; i != -1; i = G[i].next) { v = G[i].to; w = G[i].val; if(dis[v] < dis[u] + w) { dis[v] = dis[u] + w; if(!vis[v]) { vis[v] = true; q.push(v); } } } } int ans = 0; for(i = 0; i <= cnt; ++i) { ans = max(ans, dis[i]); } return ans; } int main() { //freopen("data.in", "r", stdin); int t, n, m, i, j; int cct, u, v; scanf("%d", &t); while(t--) { init(); scanf("%d%d", &n, &m); cct = 0; REP(i, n) { scanf("%s", mp[i]); REP(j, m) { if(mp[i][j] == '#') continue; num[i][j] = ++cct; if(mp[i][j] != '*') ore[cct] = mp[i][j] - '0'; } } REP(i, n) { REP(j, m) { //点1 if(mp[i][j] == '#') continue; if(mp[i][j] == '*') { scanf("%d%d", &u, &v); if(mp[u][v] != '#') add(num[i][j], num[u][v]); } if(i + 1 < n && mp[i+1][j] != '#') add(num[i][j], num[i + 1][j]); //点2 if(j + 1 < m && mp[i][j+1] != '#') add(num[i][j], num[i][j + 1]); } } FOR(i, 1, cct) { if(!dfn[i]) tarjan(i); } add2(0, scc[1], val[scc[1]]); //点3,val[scc[1]]! FOR(u, 1, cct) { for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(scc[u] != scc[v]) { add2(scc[u], scc[v], val[scc[v]]); } } } printf("%d\n", spfa()); } return 0; }
poj 3114
题意: 给你一堆城市以及这些城市之间传递消息需要的时间(城市2到城市1的所需要的时间不一定等于城市1到城市2所需要的时间),如果两个城市之间相互可以给对方传递消息,则认为这两个城市是一个国家的,则,这两个城市可以用另一种方法传递消息,花费时间为0.现在问你很多次某两个城市之间传递消息所花的最少时间是多少。
思路:题意没看懂,直接搜的翻译。知道题意这题基本就解决了。。。在一个强连通分量里的点表示同一个国家的,然后重新加图,spfa求最短路就可以。数组开小了,RE 1次。。。

#include <iostream> #include <cstdio> #include <cstring> #include <queue> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, L, H) for(i = L; i <= H; ++i) #define FORD(i, H, L) for(i = H; i >= L; --i) #define CL(arr, val) memset(arr, val, sizeof(arr)) using namespace std; const int N = 550; const int inf = ~0u>>2; struct node { int to; int val; int next; } g[N*N]; int G[N][N]; int ind, cnt, top; int head[N], t; int dfn[N], low[N]; int dis[N], scc[N]; int stk[N*N]; bool inq[N], vis[N]; int n, m; void init() { CL(head, -1); CL(G, 0); CL(dfn, 0); CL(low, 0); CL(scc, 0); CL(stk, 0); CL(vis, 0); t = ind = cnt = top = 0; } void add(int u, int v, int w) { g[t].to = v; g[t].val = w; g[t].next = head[u]; head[u] = t++; } void tarjan(int u) { vis[u] = true; dfn[u] = low[u] = ++ind; stk[top++] = u; int i, v; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(vis[v]) low[u] = min(low[u], dfn[v]); } if(low[u] == dfn[u]) { ++cnt; do { v = stk[--top]; scc[v] = cnt; vis[v] = false; } while(v != u); } } int spfa(int s, int t) { if(s == t) return 0; queue<int> q; int v, u; REP(v, N) dis[v] = inf, inq[v] = false; q.push(s); dis[s] = 0; inq[s] = true; while(!q.empty()) { u = q.front(); q.pop(); inq[u] = false; for(v = 1; v <= cnt; ++v) { if(G[u][v]) { if(dis[v] > dis[u] + G[u][v]) { dis[v] = dis[u] + G[u][v]; if(!inq[v]) { inq[v] = true; q.push(v); } } } } } return dis[t]; } int main() { //freopen("data.in", "r", stdin); int q, i, ans; int u, v, w; while(scanf("%d%d", &n, &m), n) { init(); while(m--) { scanf("%d%d%d", &u, &v, &w); add(u, v, w); } FOR(i, 1, n) if(!dfn[i]) tarjan(i); FOR(u, 1, n) { for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; w = g[i].val; if((scc[u] != scc[v] && G[scc[u]][scc[v]] == 0) || G[scc[u]][scc[v]] > w) { G[scc[u]][scc[v]] = w; } } } scanf("%d\n", &q); while(q--) { scanf("%d%d", &u, &v); ans = spfa(scc[u], scc[v]); if(ans == inf) puts("Nao e possivel entregar a carta"); else printf("%d\n", ans); } cout << endl; } return 0; }
图的割边和割点
poj 3352&&3117
题意 :给一个无向图,判断加多少无向边能构成无向双连通图;
最小割模型
poj 3308
题意:一个棋盘,每行每列可以建一杆机关枪,每杆枪都可以打到棋盘尽头。当然,每个地方的造价不同。现在有一些伞兵落在棋盘上,问建一定数目的机关枪把所有的伞兵都消灭,并且保证造这些机关枪的费用的乘积最小。
思路:这里让求乘积最小,已知log(a*b) = loga + logb。所以可以把造假转成log(val)。求和最小,也就是求最小割->最大流。
建图:已知棋盘为n*m。S与i (1 <= i <= n)相连,容量为log(val[i])。j ( n < j <= n + m) 与T相连,容量为log(val[j])。设伞兵降落的位置为(u, v),则u -> v + n相连,容量为inf。这样保证最小割在S->i,或则j -> T的边集上取到,而不会在i -> j上。
研究了一下Dinic的邻接表写法,表示很网上的代码很纠结。。。很乱

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, h) for(i = l; i <= h; ++i) #define FORD(i, h, l) for(i = h; i >= l; --i) #define CL(arr, val) memset(arr, val, sizeof(arr)) using namespace std; const int N = 3000; const double inf = ~0u; struct node { int to; double val; int next; } g[N*N]; int head[N], t; int layer[N]; int q[N*N]; int S, T; void init() { CL(head, -1); t = 0; } void add(int u, int v, double c) { g[t].to = v; g[t].val = c; g[t].next = head[u]; head[u] = t++; g[t].to = u; g[t].val = 0; g[t].next = head[v]; head[v] = t++; } bool bfs() { CL(layer, -1); int u, v, f, r, i; f = r = 0; q[r++] = S; layer[S] = 1; while(f < r) { u = q[f++]; for(i = head[u]; i != -1; i = g[i].next) { v = g[i].to; if(layer[v] == -1 && g[i].val > 0) { layer[v] = layer[u] + 1; if(v == T) return true; q[r++] = v; } } } return false; } double dfs() { int u, i, e, top, pos; double flow, sum = 0; top = 1; while(top) { u = (top == 1) ? S : g[q[top-1]].to; if(u == T) { flow = inf; FOR(i, 1, top - 1) { e = q[i]; if(g[e].val < flow) { flow = g[e].val; pos = i; } } FOR(i, 1, top - 1) { e = q[i]; g[e].val -= flow; g[e^1].val += flow; } top = pos; sum += flow; } else { for(i = head[u]; i != -1; i = g[i].next) { if(g[i].val > 0 && layer[g[i].to] == layer[u] + 1) break; } if(i != -1) q[top++] = i; else { --top; layer[u] = -1; } } } return sum; } double Dinic() { double res = 0; while(bfs()) res += dfs(); return res; } int main() { //freopen("data.in", "r", stdin); int t, n, m, l, i, u, v; double val; scanf("%d", &t); while(t--) { init(); scanf("%d%d%d", &n, &m, &l); S = 0; T = n + m + 1; FOR(i, 1, n) { scanf("%lf", &val); add(S, i, log(val)); } FOR(i, 1, m) { scanf("%lf", &val); add(i + n, T, log(val)); } while(l--) { scanf("%d%d", &u, &v); add(u, v + n, inf); } printf("%.4lf\n", exp(Dinic())); } return 0; }
KM算法
poj 2195
前边做过,建图求最小费用最大流。。。当时没看KM算法,原来还可以当成二分图来做,求最小权值匹配。建图还是那样建,给H编号,然后接着原来的号给m编号。然后用邻接矩阵建无向图,网上说的KM一般都是在求二分图最大权值匹配,可以把边的权值取反,求最大匹配。
当模板了。。

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, h) for(i = l; i <= h; ++i) #define FORD(i, h, l) for(i = h; i >= l; --i) #define CL(arr, val) memset(arr, val, sizeof(arr)) using namespace std; const int N = 1024; const int inf = ~0u>>2; int x[N], y[N]; int lx[N]; int ly[N]; int mp[N][N]; int num[N][N]; char c[N][N]; int linky[N]; int slack[N]; bool visx[N]; bool visy[N]; int n, cnt; int Abs(int x) { return x < 0 ? -x : x; } bool dfs(int x) { //Hungry算法的思想求完全匹配 int y, d; visx[x] = true; REP(y, cnt) { d = lx[x] + ly[y] - mp[x][y]; if(d == 0 && !visy[y]) { visy[y] = true; if(linky[y] == -1 || dfs(linky[y])) { linky[y] = x; return true; } } else slack[y] = min(slack[y], d); } return false; } void KM() { int i, j, k, ans = 0, d; REP(i, cnt) { ly[i] = 0; linky[i] = -1; lx[i] = -inf; REP(j, cnt) { lx[i] = max(lx[i], mp[i][j]); } } REP(k, cnt) { //对每一个点找一次完全匹配,知道整个图实现完全匹配 CL(visx, false); CL(visy, false); REP(i, cnt) slack[i] = inf; while(!dfs(k)) { d = inf; REP(i, cnt) if(!visy[i]) d = min(d, slack[i]); REP(i, cnt) { if(visx[i]) {visx[i] = false; lx[i] -= d;} if(visy[i]) {visy[i] = false; ly[i] += d;} } } } REP(i, cnt) ans += lx[i] + ly[i]; ans /= -2; cout << ans << endl; } void build(int n, int m) { int i, j; cnt = 0; CL(c, 0); REP(i, n) { scanf("%s", c[i]); REP(j, m) { if(c[i][j] == 'H') { x[cnt] = i; y[cnt++] = j; } } } int l = cnt; REP(i, n) { REP(j, m) { if(c[i][j] == 'm') { x[cnt] = i; y[cnt++] = j; } } } REP(i, cnt) REP(j, cnt) mp[i][j] = -inf; REP(i, l) { FOR(j, l, cnt - 1) { mp[j][i] = mp[i][j] = -(Abs(x[i] - x[j]) + Abs(y[i] - y[j])); } } } int main() { //freopen("data.in", "r", stdin); int n, m; while(~scanf("%d%d", &n, &m)) { if(n + m == 0) break; build(n, m); KM(); } return 0; }
poj 3686
题意:N个订单,M个机器,给出第i个订单在第j个机器完成所用的时间cost[i][j],求总耗时的平均值最小。
思路:拆点拆的很巧,比如我们一直一组数据:
N个订单,每个完成的时间为T0,T1, T2, T3...Tn-1;
所以总时间为TIME = T0*N + T1*(n-1) + T2*(N-2) + ... + Tn-1;
因为前边的单独完成一个订单所耗的时间会推迟后边的时间。
T0
T0 + T1
T0 + T1 + T2
....
T0 + T1 + T2 + .. + Tn-1
理解上边的公式拆点就简单了。设第i个订单,把第j个机器拆成N个点,这N个点中的第k个点表示第i个订单在j机器上倒数第k个顺序完成。
所有的点共 N*M个。
ps:注意KM算法O(n^3)时要dfs的点是N个,不是N*M个,因为X集合里只会有N个元素,TLE两次。。。

#include <iostream> #include <cstdio> #include <cstring> #include <queue> #define CL(arr, val) memset(arr, val, sizeof(arr)) #define REP(i, n) for(i = 0; i < n; ++i) #define FOR(i, l, h) for(i = l; i <= h; ++i) #define FORD(i, h, l) for(i = h; i >= l; --i) using namespace std; const int N = 55; const int M = N*(N+1); const int inf = ~0u>>2; int mp[N][N*N]; int cost[N][N]; int linky[M]; int lx[M], ly[M]; int slack[M]; bool vx[M]; bool vy[M]; int cnt, n; bool dfs(int t) { int u, d; vx[t] = true; REP(u, cnt) { d = lx[t] + ly[u] - mp[t][u]; if(d == 0 && !vy[u]) { vy[u] = true; if(linky[u] == -1 || dfs(linky[u])) { linky[u] = t; return true; } } else if(d < slack[u]) slack[u] = d; } return false; } int KM() { int i, j, k, d, ans = 0; REP(i, cnt) { lx[i] = 0; ly[i] = 0; linky[i] = -1; } REP(i, n) { lx[i] = -inf; REP(j, cnt) lx[i] = max(lx[i], mp[i][j]); } REP(k, n) { CL(vx, false); CL(vy, false); REP(i, cnt) slack[i] = inf; while(!dfs(k)) { d = inf; REP(i, cnt) if(!vy[i]) d = min(d, slack[i]); REP(i, cnt) { if(vy[i]) {vy[i] = false; ly[i] += d;} if(vx[i]) {vx[i] = true; lx[i] -= d;} } } } REP(i, cnt) ans += lx[i] + ly[i]; return ans; } int main() { //freopen("data.in", "r", stdin); int t, m, i, j, k; double ans; scanf("%d", &t); while(t--) { scanf("%d%d", &n, &m); REP(i, n) REP(j, m) scanf("%d", &cost[i][j]); REP(i, n) { REP(j, m) { REP(k, n) { mp[i][j*n+k] = -(k+1)*cost[i][j]; } } } cnt = n*m; ans = -1.0*KM()/n; printf("%.6f\n", ans); } return 0; }
ps: 图论这一节到这里基本刷完了,整整两周时间,又把训练计划托慢了。。。最大的感觉就是图论太广了。广一个最小割模型就有很多东西,要覆盖的东西还很多。。。跳过了两道题,3155是分数规划,在胡伯涛的《最小割模型在信息学竞赛中的应用》上讲到了,不过本菜表示还是不会。。。过段时间再看吧,貌似做图论到了一个高原期。下周搞其它的,图论这个东西还会再搞的!