搞完了网络流图,搞完了一般图,最后应该是二分图了(在本沙茶知道的图的范围内)
二分图的边有些特别,边<a, b>并不是表示从a到b的边,而是从X方点a(简记为Xa)到Y方点b(简记为Yb)的边。在二分图中,边其实是无向的,但由于大多数引用的时候都是X方点引用Y方点,所以可以把边看成有向的(如果实在要逆向引用的话,加一条逆向边吧囧……)
边类型定义(和一般图完全一致。这里是不带权的,如果像KM算法里面有带权边,加一个len域即可):
struct edge {
    
int a, b, pre, next;
} ed[MAXM];
关键是在初始化表头的时候,设图中有NX个X方点和NY个Y方点,则单点链表数其实只有NX。故只需弄NX个表头即可:
void init_d()
{
    re(i, nx) ed[i].a 
= ed[i].pre = ed[i].next = i;
    
if (nx % 2) m = nx + 1else m = nx;
}
然后是加边,和一般图完全一致:
void add_edge(int a, int b)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
}
差不多就搞完了。下面是用Dancing Link边表实现的Hungary算法的深度优先遍历部分:
bool dfs(int x)
{
    vst[x] 
= 1;
    
int y, x1;
    
for (int p=ed[x].next; p != x; p=ed[p].next) {
        y 
= ed[p].b; x1 = xz[y];
        
if (x1 == -1 || !vst[x1] && dfs(x1)) {
            xz[y] 
= x; return 1;
        }
    }
    
return 0;
}
这里xz[y]指的是y所匹配的X方点编号(若y是未盖点则xz[y]=-1),vst[x]是X方点x是否被访问的标志,在每次遍历前,vst都要全部清零。

【应用实例】PKU1087
题意很难懂,其实就是:房间里有N个插座,每个插座都有一个型号(没有两个插座的型号相同),现在要把M个用电器插到这N个插座里,每个用电器有一个默认型号,表示该用电器只能插到其默认型号的插座里(有可能有的用电器的默认型号不与房间里的任意一个插座对应,如样例中的X型号)。有K种适配器(每种适配器可以用无限次),每一种适配器都有两个参数S1和S2,表示该种适配器使用一次可以将某个默认型号为S1的用电器的默认型号改为S2(同一个用电器可以被多次改变默认型号)。问在使用了若干次适配器后,最少有多少个用电器插不到插座里?
首先将每种型号作为一个点,若某种适配器的两个参数分别为S1和S2则连边<S1, S2>,对这个有向图求传递闭包。然后再建一个二分图,X方点表示用电器,Y方点表示插座,若Xi的初始默认型号在一开始建的有向图中可以到达Yj的型号(用传递闭包直接得到),则连边(Xi, Yj)。对这个二分图求最大匹配,则(M - 最大匹配值)就是结果。
代码:
#include <iostream>
#include 
<stdio.h>
#include 
<string.h>
#include 
<string>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 500, MAXM = 250500, MAXLEN = 50, INF = ~0U >> 2;
struct edge {
    
int a, b, pre, next;
} ed[MAXM];
int n, nx, ny, m, xt[MAXN], yt[MAXN], xz[MAXN], res = 0;
bool f[MAXN][MAXN], vst[MAXN];
string nm[MAXN];
void init()
{
    
string s1, s2;
    
char s01[MAXLEN], s02[MAXLEN], ch;
    scanf(
"%d"&ny); ch = getchar();
    re(i, ny) {gets(s01); nm[i] 
= s01; yt[i] = i;} n = ny;
    scanf(
"%d"&nx); ch = getchar();
    re(i, nx) {
        scanf(
"%s %s", s01, s02); ch = getchar(); xt[i] = n;
        re(j, n) 
if (nm[j] == s02) {xt[i] = j; break;}
        
if (xt[i] == n) nm[n++= s02;
    }
    
int z, t1, t2;
    scanf(
"%d"&z); ch = getchar();
    re(i, z) {
        scanf(
"%s %s", s01, s02); ch = getchar();
        t1 
= n; re(j, n) if (nm[j] == s01) {t1 = j; break;}
        
if (t1 == n) nm[n++= s01;
        t2 
= n; re(j, n) if (nm[j] == s02) {t2 = j; break;}
        
if (t2 == n) nm[n++= s02;
        f[t1][t2] 
= 1;
    }
    re(i, n) f[i][i] 
= 1;
}
void add_edge(int a, int b)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
}
void prepare()
{
    re(k, n) re(i, n) re(j, n) f[i][j] 
= f[i][j] || f[i][k] && f[k][j];
    re(i, nx) ed[i].a 
= ed[i].pre = ed[i].next = i;
    
if (nx % 2) m = nx + 1else m = nx;
    re(i, nx) {
        
int t0 = xt[i];
        re(j, ny) 
if (f[t0][j]) add_edge(i, j);
    }
}
bool dfs(int x)
{
    vst[x] 
= 1;
    
int y, x1;
    
for (int p=ed[x].next; p != x; p=ed[p].next) {
        y 
= ed[p].b; x1 = xz[y];
        
if (x1 == -1 || !vst[x1] && dfs(x1)) {
            xz[y] 
= x; return 1;
        }
    }
    
return 0;
}
void solve()
{
    re(i, ny) xz[i] 
= -1;
    re(i, nx) {
        re(j, ny) vst[j] 
= 0;
        res 
+= dfs(i);
    }
}
void pri()
{
    printf(
"%d\n", nx - res);
}
int main()
{
    init();
    prepare();
    solve();
    pri();
    
return 0;
}
有关图的Dancing Link边表的内容就到此为止了。这真是一种有大用的数据结构。

posted @ 2011-05-08 12:04 Mato_No1 阅读(402) | 评论 (0)编辑 收藏

之前本沙茶成功地在网络流图中搞出Dancing Link边表,那么对于一般的图,是否也能用Dancing Link边表呢?答案是肯定的。

边类型(带权的,不带边权的图把len域去掉即可):
struct edge {
    
int a, b, len, pre, next;
} ed[MAXM];
初始化表头:
void init_d()
{
    re(i, n) ed[i].a 
= ed[i].pre = ed[i].next = i;
    
if (n % 2) m = n + 1else m = n;
}
加边(这里是加有向边,如果加无向边的话,再加一条边<b, a, len>即可):
void add_edge(int a, int b, int len)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].len = len; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
}
在一般的图中应用Dancing Link边表的优势:【1】能够有效处理删边删点问题;【2】在无向图中,能够很快找到一条边对应的逆向边;【3】最大的优势是:如果要从某一条单点链表(其实整个边表可以看成N个单点链表的并,N为图中的点数)的中间开始遍历,或者逆向遍历整个表的话,一般的邻接链表几乎不可能完成,一般的边表也很麻烦,这种Dancing Link边表则可以很快搞定。不过它也有缺点:空间消耗比邻接链表和一般边表大一些(不过这个影响不大)。

【应用实例】PKU1062(PKU上少有的中文题)
很水的最短路问题。将每个物品当成一个点,若j可作为i的优惠品则连边<i, j>,边权为优惠价格,然后,枚举等级限制(由于物品1是必须选的,因此设最大等级限制为lmt,物品1的等级为V,则可在[V-lmt, V]范围内枚举最低等级,最高等级就是(最低等级+lmt)),将所有不在等级限制内的点全部删除(其实不用真删除,只要设一个del数组避开它们即可),求从结点1到各结点的最短路,则min(dist[i]+cs[i])(dist[i]表示1到i的最短路,cs[i]表示直接购买物品i的价格)就是结果。
代码(2Y,一开始把solve里的g[j]弄成g[i]了囧……静态查错V5啊……神犇不要鄙视):
#include <iostream>
#include 
<stdio.h>
#include 
<queue>
#include 
<utility>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
#define re3(i, l, r) for (int i=l; i<=r; i++)
typedef pair 
<intint> i_i;
typedef priority_queue 
<i_i, vector<i_i>, greater<i_i> > pqi_i;
const int MAXN = 100, MAXM = 30000, INF = ~0U >> 2;
struct edge {
    
int a, b, len, pre, next;
} ed[MAXM];
int n, m, s, lmt, cs[MAXN], g[MAXN], dist[MAXN], res = INF;
bool del[MAXN];
pqi_i q;
void init_d()
{
    re(i, n) ed[i].a 
= ed[i].pre = ed[i].next = i;
    
if (n % 2) m = n + 1else m = n;
}
void add_edge(int a, int b, int len)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].len = len; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
}
void init()
{
    
int b0, x, y;
    scanf(
"%d%d"&lmt, &n);
    init_d();
    re(i, n) {
        scanf(
"%d%d%d"&cs[i], &g[i], &x);
        re(j, x) {
            scanf(
"%d%d"&b0, &y);
            add_edge(i, 
--b0, y);
        }
    }
}
void sol1()
{
    re(i, n) 
if (!del[i]) dist[i] = INF + 1; q.push(i_i(0, s));
    
int i, j, d0, d1;
    
while (!q.empty()) {
        d0 
= q.top().first; i = q.top().second; q.pop();
        
if (dist[i] == INF + 1) {
            dist[i] 
= d0;
            
for (int p=ed[i].next; p != i; p=ed[p].next) {
                j 
= ed[p].b;
                
if (!del[j]) {
                    d1 
= d0 + ed[p].len; q.push(i_i(d1, j));
                }
            }
        }
    }
    re(i, n) 
if (!del[i]) {
        d0 
= cs[i] + dist[i];
        
if (d0 < res) res = d0;
    }
}
void solve()
{
    
int lf, rt; s = 0;
    re3(i, 
0, lmt) {
        lf 
= g[s] - i; rt = lf + lmt;
        re(j, n) del[j] 
= g[j] < lf || g[j] > rt;
        sol1();
    }
}
void pri()
{
    printf(
"%d\n", res);
}
int main()
{
    init();
    solve();
    pri();
    
return 0;
}

posted @ 2011-05-08 10:04 Mato_No1 阅读(465) | 评论 (0)编辑 收藏

考虑这样一种网络流问题:需要对同一个图求多次最大流。则在每次求最大流之前,需要将所有边的容量全部恢复到初始值(求最大流的过程中,边的容量f值被改变了)。不过这还不算最猥琐的,有的时候,我们需要在每次求最大流之前都删去图中的一些点或一些边,或者改变某些原有的边的容量,特别是需要删点或删边的情况爆难搞。因为,一般的边表中边类型定义如下:
struct edge {
        
int a, b, f, next;
} ed[MAXM 
+ MAXM];
表示这条边起点为a,终点为b,容量为f,邻接边(就是下一条起点为a的边)的编号为next。
如果要求多次最大流,那么在每次求最大流之前就要把所有边的容量恢复到初始值,解决方法是引入一个“初始容量”域fs,在这条边刚刚被加入的时候将它的fs值和f值都设为初始给它的容量,在恢复时,只要将所有边的f值恢复到fs即可。若要改变边容量,则将该边的fs值和f值都设为改变后的容量即可。

下面来分析需要删边或删点的情况。在这种情况下,如果采用只有next的单向链表,则删除时next域极难处理,而且,在一般的边表中,还设立了表头hd,hd[a]表示边表中起点为a的第一条边的编号。因此,若删除的边<a, b, f>是起点为a的第一条边,还会影响hd[a]的值,使情况变得更为复杂。因此,必须采用双向链表!还记得Dancing Link么?边表其实也可以搞成Dancing Link,方法如下:
设图中有N个点,M条边(注意,这M条边只包括正向边,不包括反向边。由于每条正向边<a, b, f>都对应一条反向边<b, a, 0>,因此边表中边的数目其实是M+M)。首先把边表ed的0~N这(N+1)个位置(下标)空出来,作表头(表头不是边,因此在遍历边的时候不会遍历到它们)。其中,ed[0]为总表头,用于遍历ed[1..N]中每个未被删去的点;ed[1..N]为单点表头,ed[i]用来遍历图中所有以i为起点的边(和DLX中的二维DL惊人相似)。然后,若N为偶数,则空一个位置(也就是将ed[N+1]丢弃不用),这是因为我们在增广过程中需要引用到一条边对应的逆向边(正向边对应反向边,反向边对应正向边),一般认为编号为p的边对应的逆向边是p ^ 1,这样,就要求图中所有正向边的编号都是偶数,所有反向边的编号都是奇数(否则会造成混乱)。因此当N为偶数时,(N+1)为奇数,不能放置第一条正向边,需要从ed[N+2]开始放置正向边。若N为奇数则不用空位。
接下来就是边类型了。在这里,边类型一共需要包括6个域:a, b, fs, f, pre, next,表示这条边起点为a,终点为b,初始容量为fs,当前容量为f,上一条起点为a的边编号为pre,下一条起点为a的边编号为next。注意,和DL一样,整个链表是循环的,也就是我们认为表中最后一条起点为a的边的下一条邻接边编号就是a(表头),同样,a的上一条邻接边也就是这条边。
struct edge {
    
int a, b, fs, f, pre, next;
} ed[MAXM 
+ MAXM];
接下来就是几个重要过程了。
(1)初始化表头:
void init_d()
{
    re1(i, n) {ed[i].a 
= i; ed[i].b = -1; ed[i].f = 0; ed[i].pre = ed[i].next = i;}
    
if (n % 2) m = n + 1else m = n + 2;
}
这里n是图中的点数(相当于N),m是边的编号指针(相当于DLX中的nodes)
(2)添加新边:
void add_edge(int a, int b, int f)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].fs = ed[m].f = f; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
    ed[m].a 
= b; ed[m].b = a; ed[m].fs = ed[m].f = 0; ed[m].pre = ed[b].pre; ed[m].next = b; ed[b].pre = m; ed[ed[m].pre].next = m++;
}
这个和DLX类似,不解释了囧……

最后进入最核心的部分——到底如何处理删边或删点?有了DL型边表就爆好搞了:删去一条边,只要直接删去该边在DL中的位置即可:
void deledge(int No)
{
    ed[ed[No].pre].next 
= ed[No].next; ed[ed[No].next].pre = ed[No].pre;
}
恢复一条已删去的边:
void resuedge(int No)
{
    ed[ed[No].pre].next 
= ed[ed[No].next].pre = No;
}
需要删点的情况类似,对单点表头处理即可。

【具体题目】PKU1815
这题就是求有向图的字典序最小的最小点割集问题,方法是先求出最小点连通度(有关最小点连通度的求法见图的连通度问题的求法),然后按编号递增顺序枚举每个点,若删去该点(其实是删去建成的新图中该点i'到该点附加点i''之间的边)后图的最小点连通度减小,则应删去该点,否则不应删去该点。删去后,继续枚举下一个点,直到求出点割集为止。
注意,本题只有删边,没有删点,因此总表头可以不需要,直接从ed[0]开始作单点表头。此时,关于是否空位就刚好反过来了:如果N是奇数就要空位,N是偶数不空位(不过这题里由于建出的网络流图中有2*N0个结点,总是偶数,可以不管,不过本沙茶还是管这个了)。

代码(神犇不要鄙视):
#include <iostream>
#include 
<stdio.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 501, MAXM = 100000, INF = ~0U >> 2;
struct edge {
    
int a, b, fs, f, pre, next;
} ed[MAXM 
+ MAXM];
int n0, n, m = 0, s, t, start[MAXN], pr[MAXN], hs[MAXN], lev[MAXN], q[MAXN], now, flow, reslen = 0, res[MAXN];
bool vst[MAXN];
void init_d()
{
    re(i, n) {ed[i].a 
= i; ed[i].b = -1; ed[i].f = 0; ed[i].pre = ed[i].next = i;}
    
if (n % 2) m = n + 1else m = n;
}
void add_edge(int a, int b, int f)
{
    ed[m].a 
= a; ed[m].b = b; ed[m].fs = ed[m].f = f; ed[m].pre = ed[a].pre; ed[m].next = a; ed[a].pre = m; ed[ed[m].pre].next = m++;
    ed[m].a 
= b; ed[m].b = a; ed[m].fs = ed[m].f = 0; ed[m].pre = ed[b].pre; ed[m].next = b; ed[b].pre = m; ed[ed[m].pre].next = m++;
}
void init()
{
    
int x;
    scanf(
"%d%d%d"&n0, &s, &t);
    n 
= n0 << 1; s--; t += n0 - 1; init_d();
    re(i, n0) re(j, n0) {
        scanf(
"%d"&x);
        
if (i == j) {
            
if (i == s || i == t - n0) add_edge(i, i + n0, INF); else add_edge(i, i + n0, 1);
        } 
else if (x) add_edge(i + n0, j, INF);
    }
}
void aug()
{
    
int z = hs[t], i = t, p; flow += z;
    
while (i != s) {
        hs[i] 
-= z; p = pr[i]; ed[p].f -= z; ed[p ^ 1].f += z; i = ed[p].a;
        
if (!ed[p].f) now = i;
    }
}
bool dfs()
{
    re(i, n) vst[i] 
= 0; vst[s] = 1; q[0= s; lev[s] = 0;
    
int i, j, f0;
    
for (int front=0, rear=0; front<=rear; front++) {
        i 
= q[front];
        
for (int p=ed[i].next; p != i; p=ed[p].next) if (ed[p].f) {
            j 
= ed[p].b;
            
if (!vst[j]) {vst[j] = 1; q[++rear] = j; lev[j] = lev[i] + 1;}
        }
    }
    
if (!vst[t]) return 0;
    now 
= s; re(i, n) {start[i] = ed[i].next; vst[i] = 0;} hs[now] = INF;
    
bool fd;
    
while (!vst[s]) {
        
if (now == t) aug();
        fd 
= 0;
        
for (int p=start[now]; p != now; p=ed[p].next) {
            j 
= ed[p].b; f0 = ed[p].f;
            
if (lev[now] + 1 == lev[j] && !vst[j] && f0) {
                fd 
= 1; start[now] = pr[j] = p; hs[j] = hs[now] <= f0 ? hs[now] : f0; now = j; break;
            }
        }
        
if (!fd) {
            vst[now] 
= 1;
            
if (now != s) now = ed[pr[now]].a;
        }
    }
    
return 1;
}
void deledge(int No)
{
    ed[ed[No].pre].next 
= ed[No].next; ed[ed[No].next].pre = ed[No].pre;
}
void resuedge(int No)
{
    ed[ed[No].pre].next 
= ed[ed[No].next].pre = No;
}
void resu_all()
{
    re(i, n) 
for (int p=ed[i].next; p != i; p=ed[p].next) ed[p].f = ed[p].fs;
}
void solve()
{
    flow 
= 0while (dfs()) ; int f_ = flow;
    
if (!flow) {reslen = -1return;}
    re(i, m) 
if (ed[i].a + n0 == ed[i].b && ed[i].a != s && ed[i].b != t) {
        deledge(i); deledge(i 
^ 1); resu_all();
        flow 
= 0while (dfs()) ;
        
if (flow < f_) {res[reslen++= ed[i].a + 1; f_--;} else {resuedge(i ^ 1); resuedge(i);}
    }
}
void pri()
{
    
if (reslen == -1) puts("0"); else if (reslen) {
        printf(
"%d\n", reslen);
        re(i, reslen 
- 1) printf("%d ", res[i]); printf("%d\n", res[reslen - 1]);
    } 
else puts("NO ANSWER!");
}
int main()
{
    init();
    solve();
    pri();
    
return 0;
}

posted @ 2011-05-07 14:12 Mato_No1 阅读(777) | 评论 (0)编辑 收藏

【问题描述】
给出一个图G和指定的源点s、汇点t,求图中从点s到点t的第K短路。
【具体题目】PKU2449(注意:本题有一个猥琐之处:不允许空路径。也就是当s等于t时要将K值加1)
【算法】
本题有一种最朴素的办法:改造Dijkstra,一开始路径(s, 0)(这里设路径(i, l)表示从s到i的一条长度为l的路径)入队。然后,每次取队中长度最短的路径,该路径(i, l)出队,可以证明,若这是终点为i的路径第x次出队,该路径一定是图中从s到i的第x短路(若x>K则该路径已无用,舍弃)。然后从点i扩展,将扩展到的路径全部入队。这样直到终点为t的路径第K次出队即可。
该算法容易实现(借助priority_queue),但时间复杂度可能达到O(MK),需要优化。
优化:容易发现该算法其实有A*的思想,或者说,该算法其实是所有结点的估价函数h()值均为0的A*算法。为了优化此题,需要将h()值改大。显然,h(i)值可以设为从i到t的最短路径长度(容易证明它是一致的),然后g(i)=目前结点代表的路径长度,f(i)=g(i)+h(i),然后A*即可。

注意:更改路径条数应该在出队时更改,而不能在入队时更改,因为可能在该路径出队之前会有新的比它更短的路径入队。

代码(PKU2449):
#include <iostream>
#include 
<stdio.h>
#include 
<queue>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
const int MAXN = 1500, MAXM = 150000, INF = ~0U >> 2;
struct edge {
    
int kk, len, next;
} ed[MAXM], ed2[MAXM];
int n, m, s, t, k_, hd[MAXN], tl[MAXN], hd2[MAXN], tl2[MAXN], h[MAXN], q[MAXN + 1], No[MAXN], res = -1;
bool vst[MAXN];
struct qnode {
    
int i, g;
};
typedef priority_queue 
<qnode, vector<qnode> > pq;
pq z;
bool operator< (qnode q1, qnode q2)
{
    
return q1.g + h[q1.i] > q2.g + h[q2.i];
}
void init()
{
    
int a0, b0, l0;
    scanf(
"%d%d"&n, &m);
    re(i, n) hd[i] 
= tl[i] = hd2[i] = tl2[i] = -1;
    re(i, m) {
        scanf(
"%d%d%d"&a0, &b0, &l0); a0--; b0--;
        ed[i].kk 
= b0; ed[i].len = l0; ed[i].next = -1;
        
if (hd[a0] == -1) hd[a0] = tl[a0] = i; else tl[a0] = ed[tl[a0]].next = i;
        ed2[i].kk 
= a0; ed2[i].len = l0; ed2[i].next = -1;
        
if (hd2[b0] == -1) hd2[b0] = tl2[b0] = i; else tl2[b0] = ed2[tl2[b0]].next = i;
    }
    scanf(
"%d%d%d"&s, &t, &k_); --s; --t; k_ += s == t;
}
void prepare()
{
    re(i, n) {h[i] 
= INF; vst[i] = 0;} h[t] = 0; vst[t] = 1; q[0= t;
    
int i, h0, j, h1;
    
for (int front=0, rear=0!(!front && rear == n || front == rear + 1); front == n ? front = 0 : front++) {
        i 
= q[front]; h0 = h[i];
        
for (int p=hd2[i]; p != -1; p=ed2[p].next) {
            j 
= ed2[p].kk; h1 = h0 + ed2[p].len;
            
if (h1 < h[j]) {
                h[j] 
= h1;
                
if (!vst[j]) {vst[j] = 1if (rear == n) q[rear = 0= j; else q[++rear] = j;}
            }
        }
        vst[i] 
= 0;
    }
}
void solve()
{
    qnode q0; q0.i 
= s; q0.g = 0; z.push(q0);
    re(i, n) No[i] 
= 0;
    
int i, d0, j, d1;
    
while (!z.empty()) {
        i 
= z.top().i; d0 = z.top().g; z.pop();
        
if (No[i] >= k_) continue;
        No[i]
++;
        
if (i == t && No[i] == k_) {res = d0; break;}
        
for (int p=hd[i]; p != -1; p=ed[p].next) {
            j 
= ed[p].kk; d1 = d0 + ed[p].len;
            q0.i 
= j; q0.g = d1; z.push(q0);
        }
    }
}
void pri()
{
    printf(
"%d\n", res);
}
int main()
{
    init();
    prepare();
    solve();
    pri();
    
return 0;
}

posted @ 2011-05-01 15:25 Mato_No1 阅读(1185) | 评论 (0)编辑 收藏

     摘要: 动态区间最大子序和问题【问题描述】给出一个序列A[0..N-1]和M个操作,每个操作都是以下三种之一:①:查询区间最大子序和操作格式:1 l r表示:查询A[l..r]内的最大子序和(就是A[l..r]内的和最大的连续子序列的和),0<=l<=r<N;②:修改单个值操作格式:2 i x表示:将A[i]的值改为x,0<=i<N;③:修改整段值操作格式:3 l ...  阅读全文

posted @ 2011-04-24 15:50 Mato_No1 阅读(1647) | 评论 (2)编辑 收藏

【问题描述】
给出一个环形的字符串S,长度为N,现在要找到一个断开点,使得从这里断开后的字符串字典序最小。或者说,对于长度为N的字符串S[0..N-1],找到一个位置i,使得字符串S' = S[i..N-1] + S[0..i-1]的字典序最小。若存在多个这样的最优断点,则取最左边(i最小)的那个。
【Sample Input】
amandamanda
【Sample Output】
10
(从第10位断开后得到的字符串"aamandamand"的字典序是11个断开位置中最小的)

【分析】
首先将这个环形串拆开:只需将S[0..N-1]的后面再接上S[0..N-2]即可(如对于样例,可构造字符串T = "amandamandaamandamand"),则T的任意一个长度为N的子串T[i..i-N+1]就是S从第i位断开得到的字符串。此时问题就变成了:给出一个长度为(2N-1)的字符串,求出其所有长度为N的子串中字典序最小的

设F[x]为T中所有起始位小于N的长度为x的子串中字典序最小的子串的起始位(若有多个则取最左边的),如对于T="abaabaaababaabaaa",有F[0]=F[1]=0,F[2]=2,F[3]=F[4]=5……本题的目的就是求出F[N]的值。一开始已知的只有F[0]=0(长度为0的字符串都是空串,字典序都是最小的,取最左边的第0位)。

可以发现,F数组有很多重要的性质:
性质1 F[0..N]数组是单调递增的。
证明:用反证法。设存在一个值x(0<=x<N)使得F[x]>F[x+1]则根据定义,有T[F[x+1]..F[x+1]+x]<=T[F[x]..F[x]+x](这里一定不会越界,即F[x]+x的值一定不大于(2N-1),因为x<N,又根据得F[x]<N,故F[x]+x<2N),这样,必有T[F[x+1]..F[x+1]+x-1]<=T[F[x]..F[x]+x-1]。然而根据F[x]的定义又可以得到T[F[x+1]..F[x+1]+x-1]>T[F[x]..F[x]+x-1](否则F[x]的值就应该等于F[x+1]的值了),矛盾,故在F[0..N]中不可能存在任何F[x]>F[x+1]的情况,也即F[0..N]数组是单调递增的(以下将F[0..N]数组简称为F数组)。
性质2 对于任意值x(0<=x<N),必然满足F[x+1]=F[x]或F[x+1]>F[x]+x。
证明:因为前面已经证明了F数组是单调递增的,这里只需证明对于任意x(0<=x<N),不存F[x]<F[x+1]<=F[x]+x的情况即可。
这里同样用反证法。设存在一个值x(0<=x<N)使得F[x]<F[x+1]<=F[x]+x。则根据定义有T[F[x+1]..F[x+1]+x]<T[F[x]..F[x]+x]且T[F[x]..F[x]+x-1]<=T[F[x+1]..F[x+1]+x-1],这样必有T[F[x]..F[x]+x-1]=T[F[x+1]..F[x+1]+x-1]且T[F[x+1]+x]<T[F[x]+x]。设D=F[x+1]-F[x],则T[F[x]]=T[F[x]+D],因为D<=x,可得T[F[x]+D]=T[F[x]+2D],即T[F[x]]=T[F[x]+2D]。这样,T[F[x]..F[x]+x-D-1]=T[F[x]+2D..F[x]+x+D-1];又因为T[F[x]+x-D]=T[F[x]+x],而T[F[x+1]+x](即T[F[x]+x+D]])<T[F[x]+x],这样,T[F[x]+x+D]<T[F[x]+x-D],也就是,T[F[x]+2D..F[x]+x+D]<T[F[x]..F[x]+x-D]!这样可以得出,从(F[x]+2D)位开始的任意长度不小于(x-D)的子串,其字典序都小于从F[x]位开始的同样长度的子串,由于F[x]<F[x+1]<=F[x]+x,D=F[x+1]-F[x],所以有1<=D<=x,这样,F[x]的值就应该是(F[x]+2D)了,这显然不可能。所以,一开始假设的这种情况是不可能存在的,即对于任意值x(0<=x<N),必然满足F[x+1]=F[x]或F[x+1]>F[x]+x。

根据F数组的以上两个性质可以设计出本题的算法:
设目前已经求出了F[0..x-1]的值,且F[x-1]=i。首先将T[0..i-1]全部删去(因为F数组是单调递增的,F[x]的值一定不小于i),然后对T自身作扩展KMP(就是以T为模板串,T为子串的扩展KMP,相当于其预处理部分),一开始先将F[x]置为i,设第j位的匹配长度为next[j],若next[j]=x-1且T[j+x-1]<T[i+x-1],则将F[x]的值改为j,这样扫描一遍,即求出了F[x]的值。若扫描过程中未出现任何next[j]=x-1,则设所有next[j]值不小于x的最小next[j]值为y,则可以直接得到F[x..y-1]的值均等于F[x-1]。就这样直到求出F[N]的值为止。

时间复杂度:O(NÖN),可以根据性质2得到。

posted @ 2011-04-23 16:09 Mato_No1 阅读(538) | 评论 (1)编辑 收藏

KMP:给出两个字符串A(称为模板串)和B(称为子串),长度分别为lenA和lenB,要求在线性时间内,对于每个A[i](0<=i<lenA),求出A[i]往前和B的前缀匹配的最大匹配长度,记为ex[i](或者说,ex[i]为满足A[i-z+1..i]==B[0..z-1]的最大的z值)。KMP的主要目的是求B是不是A的子串,以及若是,B在A中所有出现的位置(当ex[i]=lenB时)。
【算法】
设next[i]为满足B[i-z+1..i]==B[0..z-1]的最大的z值(也就是B的自身匹配)。设目前next[0..lenB-1]与ex[0..i-1]均已求出,要用它们来求ex[i]的值。
根据ex的定义,有A[i-1-ex[i-1]+1..i-1]==B[0..ex[i-1]-1],这时,若有A[i]==B[ex[i-1]],则可以直接得到ex[i]=ex[i-1]+1(因为i-1-ex[i-1]+1即i-ex[i-1],现在由于A[i]==B[ex[i-1]],可得A[i-ex[i-1]..i]==B[0..ex[i-1]],即A[i-ex[i-1]+1-1..i]==B[0..ex[i-1]+1-1],所以ex[i]=ex[i-1]+1)。若A[i]!=B[ex[i-1]]?
设j=next[ex[i-1]-1],则根据next定义得B[ex[i-1]-j..ex[i-1]-1]==B[0..j-1],又因为A[i-ex[i-1]..i-1]==B[0..ex[i-1]-1]得A[i-j..i-1]==B[ex[i-1]-j..ex[i-1]-1],这样有A[i-j..i-1]==B[0..j-1]!也就是此时只需再比较A[i]与B[j]的值是否相等即可,若相等,可得ex[i]=j+1,若仍不相等,则更新j为next[j-1],继续比较A[i]与B[j]是否相等……直到A[i]与B[j]相等或直到j==0时,A[i]仍不等于B[j],此时ex[i]=0。边界:求ex[0]时,初始j(用来代替ex[i-1])为0。
现在还有一个问题,如何求next?显然next就是以B自身为模板串,B为子串的“自身匹配”,用类似的办法即可,唯一不同的是next[0]=lenB可以直接得到,求next[1]时,初始j(代替next[i-1])为0。
【核心代码】
    lenA = strlen(A); lenB = strlen(B);
    next[
0= lenB;
    
int j = 0;
    re2(i, 
1, lenB) {
        
while (j && B[i] != B[j]) j = next[j - 1];
        
if (B[i] == B[j]) j++;
        next[i] 
= j;
    }
    j 
= 0;
    re(i, lenA) {
        
while (j && A[i] != B[j]) j = next[j - 1];
        
if (A[i] == B[j]) j++;
        ex[i] 
= j;
    }
扩展KMP:给出模板串A和子串B,长度分别为lenA和lenB,要求在线性时间内,对于每个A[i](0<=i<lenA),求出A[i..lenA-1]与B的最长公共前缀长度,记为ex[i](或者说,ex[i]为满足A[i..i+z-1]==B[0..z-1]的最大的z值)。扩展KMP可以用来解决很多字符串问题,如求一个字符串的最长回文子串和最长重复子串。
【算法】
设next[i]为满足B[i..i+z-1]==B[0..z-1]的最大的z值(也就是B的自身匹配)。设目前next[0..lenB-1]与ex[0..i-1]均已求出,要用它们来求ex[i]的值。
设p为目前A串中匹配到的最远位置,k为让其匹配到最远位置的值(或者说,k是在0<=i0<i的所有i0值中,使i0+ex[i0]-1的值最大的一个,p为这个最大值,即k+ex[k]-1),显然,p之后的所有位都是未知的,也就是目前还无法知道A[p+1..lenA-1]中的任何一位和B的任何一位是否相等。
根据ex的定义可得,A[k..p]==B[0..p-k],因为i>k,所以又有A[i..p]==B[i-k..p-k],设L=next[i-k],则根据next的定义有B[0..L-1]==B[i-k..i-k+L-1]。考虑i-k+L-1与p-k的关系:
(1)i-k+L-1<p-k,即i+L<=p。这时,由A[i..p]==B[i-k..p-k]可以得到A[i..i+L-1]==B[i-k..i-k+L-1],又因为B[0..L-1]==B[i-k..i-k+L-1]所以A[i..i+L-1]==B[0..L-1],这就说明ex[i]>=L。又由于next的定义可得,A[i+L]必然不等于B[L](否则A[i..i+L]==B[0..L],因为i+L<=p,所以A[i..i+L]==B[i-k..i-k+L],这样B[0..L]==B[i-k..i-k+L],故next[i-k]的值应为L+1或更大),这样,可以直接得到ex[i]=L!
(2)i+k-L+1>=p-k,即i+L>p。这时,首先可以知道A[i..p]和B[0..p-i]是相等的(因为A[i..p]==B[i-k..p-k],而i+k-L+1>=p-k,由B[0..L-1]==B[i-k..i-k+L-1]可得B[0..p-i]==B[i-k..p-k],即A[i..p]==B[0..p-i]),然后,对于A[p+1]和B[p-i+1]是否相等,目前是不知道的(因为前面已经说过,p是目前A串中匹配到的最远位置,在p之后无法知道任何一位的匹配信息),因此,要从A[p+1]与B[p-i+1]开始往后继续匹配(设j为目前B的匹配位置的下标,一开始j=p-i+1,每次比较A[i+j]与B[j]是否相等,直到不相等或者越界为止,此时的j值就是ex[i]的值)。在这种情况下,p的值必然会得到延伸,因此更新k和p的值。
边界:ex[0]的值需要预先求出,然后将初始的k设为0,p设为ex[0]-1。
对于求next数组,也是“自身匹配”,类似KMP的方法处理即可。唯一的不同点也在边界上:可以直接知道next[0]=lenB,next[1]的值预先求出,然后初始k=1,p=ex[1]。

需要严重注意的是,在上述的情况(2)中,本该从A[p+1]与B[p-i+1]开始匹配,但是,若p+1<i,也就是p-i+1<0(这种情况是有可能发生的,当ex[i-1]=0,且前面的ex值都没有延伸到i及以后的时候)的话,需要将A、B的下标都加1(因为此时p必然等于i-2,如果A、B的下标用两个变量x、y控制的话,x和y都要加1)!!

【核心代码】
lenA = strlen(A); lenB = strlen(B);
    next[
0= lenB; next[1= lenB - 1;
    re(i, lenB
-1if (B[i] != B[i + 1]) {next[1= i; break;}
    
int j, k = 1, p, L;
    re2(i, 
2, lenB) {
        p 
= k + next[k] - 1; L = next[i - k];
        
if (i + L <= p) next[i] = L; else {
            j 
= p - i + 1;
            
if (j < 0) j = 0;
            
while (i + j < lenB && B[i + j] == B[j]) j++;
            next[i] 
= j; k = i;
        }
    }
    
int minlen = lenA <= lenB ? lenA : lenB; ex[0= minlen;
    re(i, minlen) 
if (A[i] != B[i]) {ex[0= i; break;}
    k 
= 0;
    re2(i, 
1, lenA) {
        p 
= k + ex[k] - 1; L = next[i - k];
        
if (i + L <= p) ex[i] = L; else {
            j 
= p - i + 1;
            
if (j < 0) j = 0;
            
while (i + j < lenA && j < lenB && A[i + j] == B[j]) j++;
            ex[i] 
= j; k = i;
        }
    }
【时间复杂度分析】
在KMP和扩展KMP中,不管是A串还是B串,其匹配位置都是单调递增的,故总时间复杂度是线性的,都为O(lenA + lenB)(只是扩展KMP比KMP的常数更大一些)。
【应用】
KMP和扩展KMP在解决字符串问题中有大用。很多看上去很猥琐的字符串问题,都可以归结到这两种算法之中。另外,这里的“字符串”可以延伸为一切类型的数组,而不仅仅是字符数组。

posted @ 2011-04-17 19:11 Mato_No1 阅读(5797) | 评论 (2)编辑 收藏

放了快2个月了……(准确来说放了2年了,本沙茶从2年前就开始捉这道猥琐题……)
DLX重复覆盖的几点说明:
(1)必须有启发函数优化,否则TLE;
(2)不需要二分下界,只要将目前f值(f=g+h,即实际深度加上启发函数值)与目前求得的最优解比较即可,f>=最优解即剪枝;
(3)删一整列(delcol)操作时,可以从任意一个结点开始删(不一定要向精确覆盖那样非要从列头开始),但是,开始的那个结点不删!恢复(resucol)时也不恢复开始的那个结点!这是为了在接下来的横向遍历中不受影响。由于不删开始结点,所以在将最优列删除时,需要循环一次删除一次,恢复一次。

代码:(RQNOJ P89)

#include <iostream>
#include 
<stdio.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
#define re1(i, n) for (int i=1; i<=n; i++)
#define re3(i, l, r) for (int i=l; i<=r; i++)
const int MAXN = 61, MAXM = 61, INF = ~0U >> 2;
struct DLnode {
    
int r, c, U, D, L, R;
} d[MAXN 
* MAXM];
int n, m, nodes, rowh[MAXN], cols[MAXM], res = INF;
void init_d()
{
    re3(i, 
0, m) {
        d[i].r 
= 0; d[i].c = 0; d[i].U = d[i].D = i; d[i].L = i - 1; d[i].R = i + 1;
    }
    d[
0].L = m; d[m].R = 0;
    memset(rowh, 
0, n + 1 << 2); memset(cols, 0, m + 1 << 2); nodes = m + 1;
}
void add_node(int r, int c)
{
    d[nodes].r 
= r; d[nodes].c = c; d[nodes].U = d[c].U; d[nodes].D = c; d[c].U = nodes; d[d[nodes].U].D = nodes;
    
int rh = rowh[r];
    
if (rh) {
        d[nodes].L 
= d[rh].L; d[nodes].R = rh; d[rh].L = nodes; d[d[nodes].L].R = nodes;
    } 
else d[nodes].L = d[nodes].R = rowh[r] = nodes;
    cols[c]
++; nodes++;
}
void init()
{
    scanf(
"%d%d"&m, &n);
    init_d();
    
int k, x;
    re1(i, n) {
        scanf(
"%d"&k);
        re(j, k) {
            scanf(
"%d"&x); add_node(i, x);
        }
    }
}
void delUD(int x)
{
    d[d[x].U].D 
= d[x].D; d[d[x].D].U = d[x].U;
}
void resuUD(int x)
{
    d[d[x].U].D 
= d[d[x].D].U = x;
}
void delLR(int x)
{
    d[d[x].L].R 
= d[x].R; d[d[x].R].L = d[x].L;
}
void resuLR(int x)
{
    d[d[x].L].R 
= d[d[x].R].L = x;
}
void delcol(int c)
{
    
for (int i=d[c].D; i != c; i = d[i].D) delLR(i);
}
void resucol(int c)
{
    
for (int i=d[c].U; i != c; i = d[i].U) resuLR(i);
}
int h()
{
    
bool vst[MAXM]; memset(vst, 0, m + 1);
    
int z = 0;
    
for (int i=d[0].R; i; i = d[i].R) if (!vst[i]) {
        vst[i] 
= 1; z++;
        
for (int j=d[i].D; j != i; j = d[j].D) for (int k=d[j].R; k != j; k = d[k].R) vst[d[k].c] = 1;
    }
    
return z;
}
void dfs(int v)
{
    
if (v + h() >= res) return;
    
if (!d[0].R) {res = v; return;}
    
int min = INF, x;
    
for (int i=d[0].R; i; i = d[i].R) if (cols[i] < min) {min = cols[i]; x = i;}
    
for (int i=d[x].D; i != x; i = d[i].D) {
        delcol(i);
        
for (int j=d[i].R; j != i; j = d[j].R) delcol(j);
        dfs(v 
+ 1);
        
for (int j=d[i].L; j != i; j = d[j].L) resucol(j);
        resucol(i);
    }
}
void pri()
{
    printf(
"%d\n", res);
}
int main()
{
    init();
    dfs(
0);
     pri();
    
return 0;
}

总结:DLX精确覆盖和重复覆盖其实是有大用的,很多搜索问题都可以转化为这两种问题,效率奇高无比,且写起来也很容易(只是在建模的时候可能有点猥琐,下面的模板,和网络流一样,10min的事),至于NOIP2009引出的数独系列问题,精确覆盖可以直接秒杀。

posted @ 2011-04-12 22:27 Mato_No1 阅读(749) | 评论 (0)编辑 收藏

图的连通度问题是指:在图中删去部分元素(点或边),使得图中指定的两个点s和t不连通(不存在从s到t的路径),求至少要删去几个元素。
图的连通度分为点连通度和边连通度:
(1)点连通度:只许删点,求至少要删掉几个点(当然,s和t不能删去,这里保证原图中至少有三个点);
(2)边连通度:只许删边,求至少要删掉几条边。
并且,有向图和无向图的连通度求法不同,因此还要分开考虑(对于混合图,只需将其中所有的无向边按照无向图的办法处理、有向边按照有向图的办法处理即可)。

【1】有向图的边连通度:
这个其实就是最小割问题。以s为源点,t为汇点建立网络,原图中的每条边在网络中仍存在,容量为1,求该网络的最小割(也就是最大流)的值即为原图的边连通度。

【2】有向图的点连通度:
需要拆点。建立一个网络,原图中的每个点i在网络中拆成i'与i'',有一条边<i', i''>,容量为1(<s', s''>和<t', t''>例外,容量为正无穷)。原图中的每条边<i, j>在网络中为边<i'', j'>,容量为正无穷。以s'为源点、t''为汇点求最大流,最大流的值即为原图的点连通度。

说明:最大流对应的是最小割。显然,容量为正无穷的边不可能通过最小割,也就是原图中的边和s、t两个点不能删去;若边<i, i''>通过最小割,则表示将原图中的点i删去。

【3】无向图的边连通度:
将图中的每条边(i, j)拆成<i, j>和<j, i>两条边,再按照有向图的办法(【1】)处理;

【4】无向图的点连通度:
将图中的每条边(i, j)拆成<i, j>和<j, i>两条边,再按照有向图的办法(【2】)处理。

posted @ 2011-04-05 16:23 Mato_No1 阅读(3023) | 评论 (0)编辑 收藏

有向无环图的最小路径覆盖问题包括两种(设G是一个有向无环图,S是G的一个路径集合):
(1)最小点路径覆盖:满足对于G中所有的点i,i在S中的一条路径中出现,且只在S中的一条路径中出现,求S的最小容量;
(2)最小边路径覆盖:满足对于G中所有的边E,E在S中的一条路径中出现,且只在S中的一条路径中出现,求S的最小容量;

(1)最小点路径覆盖:
建立一个新图,将G中的每个点i在新图中拆成两个点i'、i'',若G中存在边<i, j>则在新图中连边<i', j''>,显然新图是一个二分图,求其最大匹配,则(N-新图最大匹配的值)就是最小点路径覆盖值。
时间复杂度:O(NM)(Hungary算法)

(2)最小边路径覆盖:
对于图中的每个点i,设D[i]为(i的入度-i的出度)的值,按照D[i]将图中的点分类:D[i]<0的称为“入少出多”的点,D[i]>0的称为“出少入多”的点,D[i]=0的称为“入出相等”的点。则有:
定理 有向无环图中最小边路径覆盖的值等于图中所有“入少出多”的点的D值之和。
证明:
其实只需证明:对于一个至少有一条边的有向无环图,必然存在一条路径,其起点是“入少出多”的点,终点是“出少入多”的点,所有中间点都是“入出相等”的点(只要不断的在图中找到并删去这条路径,直到图中无边为止)。
首先,由于图中无环,一定存在“入少出多”的点和“出少入多”的点。
然后,假设图中所有“入少出多”的点的后继(注意:后继和后代是不同的,一个点的后代包括这个点的所有后继、所有后继的后继、所有后继的后继的后继……)也都是“入少出多”的点,则图中所有“入少出多”的点构成了一个闭合子图,在这个闭合子图中,由于所有的点都是“入少出多”的,整个子图的入度之和必然大于出度之和,这是不可能的(有向图中的所有点入度之和必然等于所有点出度之和),故可得:图中必然存在至少一个“入少出多”的点,它有不是“入少出多”的后继。
这样,在这些拥有不是“入少出多”的后继的点中选择一个点i,将其作为路径的起点,在它的不是“入少出多”的后继中选出一个点j,若j是“出少入多”的点,则边<i, j>就是符合条件的一条路径,结束;若这样的j都是“入出相等”的点,则考虑j的后代:j的后继可能都是“入少出多”的,但j的后代中必然存在至少一个点j'不是“入少出多”的(否则j的所有后代也将构成全都是“入少出多”的闭合子图),这些j'中必然存在一个点的前趋i'是“入少出多”的,这是,需要舍弃原来的路径,将i'作为新路径的起点,j'作为新路径的第一个中间点,继续找;若j的后继不全是“入少出多”的,则若其中有“出少入多”的则路径已找到,若都是“入出相等”的,由于图中无环,将路径不断延伸,最终一定会找到合法路径。

由此可得,对于任何有向无环图,这样的路径都是存在的,也就证明了一开始的求最小边路径覆盖值的定理。
求有向无环图最小边路径覆盖值的时间复杂度:O(M+N)。

posted @ 2011-04-05 16:04 Mato_No1 阅读(2056) | 评论 (0)编辑 收藏

仅列出标题
共12页: First 4 5 6 7 8 9 10 11 12