您的位置:首页 > 其它

普林斯顿大学算法第一周个人总结2

2013-09-01 15:51 323 查看
第一周的编程作业是实现一个Percolation渗透模型。

模型描述:

有一个四方的模型,由 N*N 个区域(site)组成,每个区域有两个状态,开启(open)或关闭(blocked),相邻的开启区域能构成一条通路,当最上层区域能够通过开启区域连成的通路,和最下层互通的时候,则整个模型为渗透状态(percolated)。



如上图所示,白色和蓝色的区域为开启状态,黑色的为关闭状态,当开启的白色区域和顶层互通的时候,称为区域满(full),左侧顶层和底层能够互通,则整个模型已渗透,右侧则是非渗透的情况。

这种模型应用在实际中可类比渗水、导电等。

编程作业的要求则是实现这么一种模型,并计算出每次模型渗透时,开启区域占总区域个数的百分比,这个百分比近似于某一常数,通过多次模拟采样计算该常数。

模型分析:

(1)数据结构:给定 N 的个数,可以得到一个 N * N 个区域的模型,如果将此模型的每个区域从0进行编号,可以得到 0 ~ N^2-1 的数据,将此模型平坦成一维模型,就是一个数组的形式。

(2)数据操作:

a. 如何模拟区域的开启:开启某一个区域,将其状态标记为开启,然后四周如果有开启的区域,应该将它们连在一起。

b. 如何模拟模型的渗透:当顶层的任一区域和底层的任一区域连接在一起时,则模型渗透。实际操作时,这是一个双重for循环,可以虚拟两个区域,一个和顶层所有开启的区域互连,一个和底层所有开启的区域互连,当这两个区域连接在一起时,则模型为渗透状态,避免了双重for,N * N的数组访问操作。

作业只能用 Java 语言提交,除了此要求以外,还有其他诸如变量命名等程序风格规范、内存使用限制、运行时间限制等。完成作业以后,我将整个模型用 C 又实现了一次,并用 C 重写了算法的实现(参见个人总结1)。这篇算是个人的心得体会。

从 C 转向 Java 这个面向过程语言的时候,许多地方都不适应,磕磕绊绊在 Java 程序的编写上耽误了不少时间。作业提交以后,尝试用 C 重新完成这个任务的时候,对抽象数据类型 ADT(Abstract Data Type) 的运用感觉更加清晰。按照 Java 作业的要求,渗透模型需要完成的操作有:

public class Percolation {
public Percolation(int N)              // create N-by-N grid, with all sites blocked
public void open(int i, int j)         // open site (row i, column j) if it is not already
public boolean isOpen(int i, int j)    // is site (row i, column j) open?
public boolean isFull(int i, int j)    // is site (row i, column j) full?
public boolean percolates()            // does the system percolate?
}


ADT 是对程序正交性的实现,要求尽量将各模块之间的耦合度降低,使得修改某个模块的实现不会影响到其外部的调用。在忽略算法实现的前提下,将模型数据结构放在 C 文件,头文件中只提供调用模块的类型以及模块调用结构。

模块数据结构的内容(percolate.c文件):

struct percolation {
QuickFind   grid;           /* data structure */
char*       site_state;     /* record the state of every site */
int         n;              /* grid side count */
};


模块提供的外部类型和接口(percolation.h文件):

struct percolation;
typedef struct percolation *Perco;

/* initialize a percolation model */
Perco perco_init(int n);

/* open site of position(x, y) in percolation model.
* Position (0,0) is on the topleft */
int perco_open(Perco pl, int x, int y);

/* return true if (x,y) site is opened */
int is_open(Perco pl, int x, int y);

/* return true if (x,y) site is full */
int is_full(Perco pl, int x, int y);

/* return true if the model is percolated */
int is_percolated(Perco pl);

/* end the percolation model, must be called
* after you are done with the model */
void perco_end(Perco pl);


虽然 C 做不到严格意义上的面向对象的封装,但是这些接口的提供也足以使调用者完全不用关心内部实现。头文件(percolation.h)

struct percolation;


一行,防止编译器的警告,预先声明这样一个类型,类型的实现则完全隐藏在C文件中。
设计好这样的结构之后,先写测试程序,然后完成一个模型实现的框架,就可以继续完善模型的实现部分。

之后在渗透模型需要调用的算法实现时,也同样按照这个思想,让渗透模块完全不用关心算法的具体实现,只需要调用相应的接口完成相能的功能即可。

假设渗透模型使用 Quick-Find 算法来维护管理所有的区域。

算法的数据结构实现(quick-find.c 文件):

struct qf {
int*    qf_array;
int     count;
};


算法提供的外部接口(quick-find.h 文件):

struct qf;
typedef struct qf* QuickFind;

/* Initialize the data structure, must be called first */
QuickFind qf_init(int n);

/* connect two components to which p and q belong */
int qf_union(QuickFind qf, int p, int q);

/* check whether p and q elements are connected */
int qf_connected(QuickFind qf, int p, int q);

/* must be called after you are done with the data structure */
void qf_end(QuickFind qf);


这样的好处很明显,假如一开始采用 Quick-Union算法来实现,即使后来觉得算法效率不高,修改为Weighted-Quick-Union实现,只要接口不变,那么渗透模型完全不需要做任何改动。

详细算法的实现不在此贴出,我已经将代码全部推送到个人在 GitHub 的远程仓库,页面地址:https://github.com/Revil/algs-percolation/tree/master,仓库地址:git@github.com:Revil/algs-percolation.git,总共有四大分支,主线
master 是采用 Java 语言实现的。三种算法的 C 实现分别放在了三个不同的分支。下面是分支的列表:

c-implementation-quick-find
c-implementation-quick-union
c-implementation-weighted-quick-union
* master


在总结1的时候提到三种算法的执行效率问题。我用一个模型渗透的测试程序,测试传入不同 N 值从模型全封闭到模型开启所用的时间。

这是测试程序的主要实现部分:

clock_t ts, te;

if (argc < 2) {
fprintf(stderr, "Usage: %s <n>\n", argv[0]);
exit(EXIT_FAILURE);
}
n = atoi(argv[1]);
printf("n = %d\n", n);

srand(time(NULL));

ts = clock();

pl = perco_init(n);
do {
p = rand() % n + 1;
q = rand() % n + 1;
if (is_open(pl, p, q))
continue;
perco_open(pl, p, q);
} while (!is_percolated(pl));
perco_end(pl);

te = clock();
printf("Time elasped: %f\n", (double) (te - ts) / CLOCKS_PER_SEC);


测试的效果如下:

NQuick-Find(s)Quick-Union(s)Weighted-Quick-Union(s)
2003.070.040.01
40051.530.210.04
800...2.270.14
1600...17.530.75
Quick-Find 测试的时候,传入 800 的时偶迟迟不见程序结束,后来直接强制关闭程序了,根据从200到400的时间增长的趋势来看,估计要运行数星期的时间(是星期,不是小时),1600 就更不用测试了。而到 Quick-Union 的时候,消耗时间已经显著下降,到 Weighted-Quick-Union 的时候,即使数量成四倍的增长(注意传入的N值在渗透模型种使用的是 N * N,所以对算法来说,实际增长数不只是翻倍而已),所用时间也没有很明显的增加。

附言:
关于这个模型,其实存在一个问题,在算法课程的论坛上,讨论的热度很高。问题是这样的:

由于引入虚拟的顶层区域和虚拟的底层区域,那么当模型渗透的时候,可能会出现下图的情况



如右边图所示,由于所有的底层区域都和虚拟底层区域相连,所以一旦当区域渗透,则和其他的底层开启区域相连的区域也显示为区域满状态。而实际的情况应该是按照左图所示。这个问题称为 backwash,个人把这个翻译成“回流”。引入虚拟底层区域,很难避免这个问题。讨论的结果,有两种方式可以改进:

1. 不使用虚拟底层区域,只保留顶层,判断是否渗透的时候用虚拟顶层和一个for循环来判断。

2. 保留虚拟底层区域,另外加一个不使用虚拟底层的模型,将两个模型结合在一起来判断是否渗透,通过浪费一些内存来保证效率。

由于作业要求 isOpen 和 isFull 这两个接口算法效率都只能是常数值,所以后来大家只能采用看起来很奇怪的第2种方式来实现(backwash测试不通过的话只能拿87分)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: