随笔 - 70  文章 - 160  trackbacks - 0

公告:
知识共享许可协议
本博客采用知识共享署名 2.5 中国大陆许可协议进行许可。本博客版权归作者所有,欢迎转载,但未经作者同意不得随机删除文章任何内容,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言。

常用链接

留言簿(8)

随笔档案

文章档案

搜索

  •  

积分与排名

  • 积分 - 176479
  • 排名 - 147

最新评论

阅读排行榜

评论排行榜

上一篇:http://www.cppblog.com/tanky-woo/archive/2011/04/09/143794.html

前三章基本没什么内容,所以合在一起总结。

第一章:

讲了算法(algorithm)的基本概念,以及算法的作用。(这些可以看书)

用个人的话来讲,你可以把算法当做一个解决问题的方法,就像数学里的各种公式一样,你也可以把他们认为是一种算法。算法无处不在,而且算法必须存在,否则我们的生活都将变得缓慢,迟钝。

举个例子:我们平时出去游玩时,要事先查好路线,这时就可以用百度地图搜索从A地到B地的路线,地图上会给出最快的乘车路线,这些路线是怎么给出来的,就是用了最短路的算法,关于最短路的算法有很多,比如Dijkstra, Bellman, Floyd, SPFA等等,当然还有好多我不知道,但是通过这可以看出,算法可以让我们的生活变得更有效率。

当然,第一章也可以认为是给大家鼓气的一章,让大家发现算法的魅力,算法的强悍。大家都来爱上算法吧!

 

 

第二章:

本书的算法都是用伪代码写的,伪代码读起来很简单,它省去了无关的细节,着重考虑算法的整体。

2.1节讲的是插入排序(Insertion Sort),这个很简单,也可以认为是最基本的排序算法。

(P11)需要好好的记住,一般一本书中都会写一些事先的约定,以方便大家阅读。本书也不例外,这些约定都是关于伪代码的,为了更好的阅读并理解伪代码,所以这些约定要记住了!

2.2节讲的是算法的分析。算法分析是指对一个算法所需要的资源进行预测。在(P13)讲到了"运行时间"和"输入规模"的概念。一个程序的运行时间可以表示为一个输入规模的函数。一般算法所需的时间与输入规模是同步增长的,而且对于不同的输入序列,其运行时间也可能不同。(P14~15的算法运行时间分析要好好看看)。

2.3节讲的是分治法。

分治策略:将原问题划分成n个规模较小而结构与原问题相似的子问题;递归的解决这些子问题,然后再合并其结果,就得到原问题的解。

分治策略的三步骤(P17):分解(Divide),解决(Conquer),合并(Combine)。

合并排序算法就是利用了分治策略,将n个元素分成各含n/2个元素的子序列。

这个是分治法的精髓:

mergesort 

其实理解起来很简单,有没有发现和二叉树的后序遍历类似。

 

 

第三章:

一般而言,我们研究的是算法的渐进意义。我在这里把渐进确界渐进上界渐进下界的三个符号的定义放在了一起:

jianjinfuhao书上的图3-1也非常给力:

jianjin 

这一章全部很重要。可以先记住,然后在后面的章节通过实践来掌握

 

Tanky Woo 标签:
posted @ 2011-04-10 09:53 Tanky Woo 阅读(2846) | 评论 (4)编辑 收藏

09年买的这本书,不过先开始一直没怎么用,直到去年6月份左右开始搞ACM,才偶尔翻翻这本书。

这本书给我这样的感觉:有时遇到一个算法,在网上找了很多相关资料,但是看完后还是有点迷茫,然后才想起《算法导论》,遇到翻开目录,发现有相关的章节,于是去认真阅读,顿时发现自己的很多问题都可以解决了。它就是这么一本书,也许你会把它当一本圣经来供养,但是当你认真阅读后,你会发现你受益颇多。

于是,自从几次问题通过《算法导论》解决后,我开始意识到,这是一个多么大的宝库啊。它容纳的目前常用的诸多算法,并且都给予了详细解释,图文并茂,易于理解。

到目前为止,中间零散的看过一些章节。我有这么一个习惯,就是每学到一个算法,我都会把这个算法总结一下,我觉得虽然当时写这个总结时有点占用时间,但是当你再温故时,你只需要短短的5分钟,就可以再次记住这个算法,并且通过以前总结的,你也许还可以发现不足,并改正。就像我之前有两次都中断了算法的学习,并且一断就是几个月,但是当我再次拾起算法时,我只需要看看我以前总结的笔记,就可以很快的拾起。在这里推荐下我的算法专题:http://www.wutianqi.com/sfzt.html

所以,这次我也准备把《算法导论》这本书好好总结一下,这样当我总结时,我就可以知道哪些我彻底掌握了,因为如果我只掌握其表面内容,我是没法用自己的话去总结。二是这样大家通过各种搜索引擎搜到我的文章时,可以互相探讨,并且发现哪些地方我理解错了,因为我是非计科生,从大一到现在都是我自己一个人自学过来的,中间肯定会走一些弯路,对一些知识产生曲解,所以这样也可以通过大家来改正我的错误,大家互相学习,互相探讨,交一个可以讨论学术的朋友,何乐而不为呢?

网上有些朋友推荐再遇到算法问题时可以把《算法导论》当字典来查,但是个人觉得,这本书读多少遍都值得,所以完完整整的把这本书看一遍是必须的,以后再可以当一个工具书去查。所以推荐大家一起好好把《算法导论》学习下。

另外,我推荐每学一个算法,就去各个OJ(Online Judge)找一些相关题目做做,有时理论让人很无语,分析代码也是一个不错的选择。


对于接下来要写的一些总结文章,我想做一些约定

1.写这个总结,我不能确定时间,也许一天总结一章,也许几天总结一章,但是我不会断的,鉴于书上有35章,我估计最快得两个月的时间,最慢应该会花3个月在暑期之前搞定。(因为我还得准备考研,悲催啊。。。)

2.对于后序总结,我不会照搬书上去写,我会尽量少写书上已有的一些原话,我要写的会是强调一些重点,以及书上讲解过少的地方,我会找资料补充起来。

3.当然事情不是绝对的,对一些很重要的概念,算法等,我会根据书上及其他资料再写一遍。

4.对于一些书上内容,当我借鉴时,我可能会直接从英文版的chm书上直接copy或者截图(比如有特殊符号)。

5.因为我只看过部分章节,还有一些章节我也是小白,所以肯定会有一些地方自己也迷糊,大家发现我哪些地方讲的不足,如果发现网上有讲的详细的,可以把网址通过留言告诉我,谢谢。

6.如果我文章里的哪些文字伤害了你的心灵,你可以留言告诉我,我会尽力去安慰你。(呵呵,开个玩笑),大家和谐讨论,和谐学习,和谐共处

7.以后有需要补充的约定我还会再添加。。。


 

接下来,就让我们大家一起来学习《算法导论》吧!

再废话一句:

推荐论坛:C++奋斗乐园(C/C++/算法):http://www.cppleyuan.com/

推荐QQ群:119154131(里面有一些高校的牛。。。加群请填写ACM验证

http://www.wutianqi.com/?p=2298
posted @ 2011-04-09 12:13 Tanky Woo 阅读(3133) | 评论 (9)编辑 收藏

接上一篇:最短路径算法—Bellman-Ford(贝尔曼-福特)算法分析与实现(C/C++)

Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。

  Dijkstra算法是很有代表性的最短路算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。

其基本思想是,设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。

初始时,S中仅含有源。设u是G的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。

例如,对下图中的有向图,应用Dijkstra算法计算从源顶点1到其它顶点间最短路径的过程列在下表中。



Dijkstra算法的迭代过程:

主题好好理解上图!

以下是具体的实现(C/C++):

/***************************************
* About:    有向图的Dijkstra算法实现
* Author:   Tanky Woo
* Blog:     www.WuTianQi.com
**************************************
*/
 
#include 
<iostream>
using namespace std;
 
const int maxnum = 100;
const int maxint = 999999;
 
 
void Dijkstra(int n, int v, int *dist, int *prev, int c[maxnum][maxnum])
{
    
bool s[maxnum];    // 判断是否已存入该点到S集合中
    for(int i=1; i<=n; ++i)
    {
        dist[i] 
= c[v][i];
        s[i] 
= 0;     // 初始都未用过该点
        if(dist[i] == maxint)
            prev[i] 
= 0;
        
else
            prev[i] 
= v;
    }
    dist[v] 
= 0;
    s[v] 
= 1;
 
    
// 依次将未放入S集合的结点中,取dist[]最小值的结点,放入结合S中
    
// 一旦S包含了所有V中顶点,dist就记录了从源点到所有其他顶点之间的最短路径长度
    for(int i=2; i<=n; ++i)
    {
        
int tmp = maxint;
        
int u = v;
        
// 找出当前未使用的点j的dist[j]最小值
        for(int j=1; j<=n; ++j)
            
if((!s[j]) && dist[j]<tmp)
            {
                u 
= j;              // u保存当前邻接点中距离最小的点的号码
                tmp = dist[j];
            }
        s[u] 
= 1;    // 表示u点已存入S集合中
 
        
// 更新dist
        for(int j=1; j<=n; ++j)
            
if((!s[j]) && c[u][j]<maxint)
            {
                
int newdist = dist[u] + c[u][j];
                
if(newdist < dist[j])
                {
                    dist[j] 
= newdist;
                    prev[j] 
= u;
                }
            }
    }
}
 
void searchPath(int *prev,int v, int u)
{
    
int que[maxnum];
    
int tot = 1;
    que[tot] 
= u;
    tot
++;
    
int tmp = prev[u];
    
while(tmp != v)
    {
        que[tot] 
= tmp;
        tot
++;
        tmp 
= prev[tmp];
    }
    que[tot] 
= v;
    
for(int i=tot; i>=1--i)
        
if(i != 1)
            cout 
<< que[i] << " -> ";
        
else
            cout 
<< que[i] << endl;
}
 
int main()
{
    freopen(
"input.txt""r", stdin);
    
// 各数组都从下标1开始
    int dist[maxnum];     // 表示当前点到源点的最短路径长度
    int prev[maxnum];     // 记录当前点的前一个结点
    int c[maxnum][maxnum];   // 记录图的两点间路径长度
    int n, line;             // 图的结点数和路径数
 
    
// 输入结点数
    cin >> n;
    
// 输入路径数
    cin >> line;
    
int p, q, len;          // 输入p, q两点及其路径长度
 
    
// 初始化c[][]为maxint
    for(int i=1; i<=n; ++i)
        
for(int j=1; j<=n; ++j)
            c[i][j] 
= maxint;
 
    
for(int i=1; i<=line; ++i)  
    {
        cin 
>> p >> q >> len;
        
if(len < c[p][q])       // 有重边
        {
            c[p][q] 
= len;      // p指向q
            c[q][p] = len;      // q指向p,这样表示无向图
        }
    }
 
    
for(int i=1; i<=n; ++i)
        dist[i] 
= maxint;
    
for(int i=1; i<=n; ++i)
    {
        
for(int j=1; j<=n; ++j)
            printf(
"%8d", c[i][j]);
        printf(
"\n");
    }
 
    Dijkstra(n, 
1, dist, prev, c);
 
    
// 最短路径长度
    cout << "源点到最后一个顶点的最短路径长度: " << dist[n] << endl;
 
    
// 路径
    cout << "源点到最后一个顶点的路径为: ";
    searchPath(prev, 
1, n);
}

输入数据:
5
7
1 2 10
1 4 30
1 5 100
2 3 50
3 5 10
4 3 20
4 5 60
输出数据:
999999 10 999999 30 100
10 999999 50 999999 999999
999999 50 999999 20 10
30 999999 20 999999 60
100 999999 10 60 999999
源点到最后一个顶点的最短路径长度: 60
源点到最后一个顶点的路径为: 1 -> 4 -> 3 -> 5

最后给出两道题目练手,都是直接套用模版就OK的:
1.HDOJ 1874 畅通工程续
http://www.wutianqi.com/?p=1894

2.HDOJ 2544 最短路
http://www.wutianqi.com/?p=1892

posted @ 2011-01-19 13:06 Tanky Woo 阅读(22530) | 评论 (7)编辑 收藏

相关文章:

1.Dijkstra算法:

http://www.wutianqi.com/?p=1890

2.Floyd算法:

http://www.wutianqi.com/?p=1903

Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的。这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个。该算法由美国数学家理查德•贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特•福特(Lester Ford)发明。Bellman-Ford算法的流程如下:
给定图G(V, E)(其中V、E分别为图G的顶点集与边集),源点s,

  • 数组Distant[i]记录从源点s到顶点i的路径长度,初始化数组Distant[n]为, Distant[s]为0;
  •  
    以下操作循环执行至多n-1次,n为顶点数:
    对于每一条边e(u, v),如果Distant[u] + w(u, v) < Distant[v],则另Distant[v] = Distant[u]+w(u, v)。w(u, v)为边e(u,v)的权值;
    若上述操作没有对Distant进行更新,说明最短路径已经查找完毕,或者部分点不可达,跳出循环。否则执行下次循环;
  • 为了检测图中是否存在负环路,即权值之和小于0的环路。对于每一条边e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的边,则图中存在负环路,即是说改图无法求出单源最短路径。否则数组Distant[n]中记录的就是源点s到各顶点的最短路径长度。

可知,Bellman-Ford算法寻找单源最短路径的时间复杂度为O(V*E).

首先介绍一下松弛计算。如下图:


 

松弛计算之前,点B的值是8,但是点A的值加上边上的权重2,得到5,比点B的值(8)小,所以,点B的值减小为5。这个过程的意义是,找到了一条通向B点更短的路线,且该路线是先经过点A,然后通过权重为2的边,到达点B。
当然,如果出现一下情况


 

则不会修改点B的值,因为3+4>6。
 
Bellman-Ford算法可以大致分为三个部分
第一,初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的值设为无穷大(表示不可达)。
第二,进行循环,循环下标为从1到n-1(n等于图中点的个数)。在循环内部,遍历所有的边,进行松弛计算。
第三,遍历途中所有的边(edge(u,v)),判断是否存在这样情况:
d(v) > d (u) + w(u,v)
则返回false,表示途中存在从源点可达的权为负的回路。
 
之所以需要第三部分的原因,是因为,如果存在从源点可达的权为负的回路。则 应为无法收敛而导致不能求出最短路径。
考虑如下的图:
 

经过第一次遍历后,点B的值变为5,点C的值变为8,这时,注意权重为-10的边,这条边的存在,导致点A的值变为-2。(8+ -10=-2)
 
 

第二次遍历后,点B的值变为3,点C变为6,点A变为-4。正是因为有一条负边在回路中,导致每次遍历后,各个点的值不断变小。
 
在回过来看一下bellman-ford算法的第三部分,遍历所有边,检查是否存在d(v) > d (u) + w(u,v)。因为第二部分循环的次数是定长的,所以如果存在无法收敛的情况,则肯定能够在第三部分中检查出来。比如
 

此时,点A的值为-2,点B的值为5,边AB的权重为5,5 > -2 + 5. 检查出来这条边没有收敛。
 
所以,Bellman-Ford算法可以解决图中有权为负数的边的单源最短路径问。

个人感觉算法导论讲解很不错,把这一章贴出来和大家分享:

24.1 The Bellman-Ford algorithm

The Bellman-Ford algorithm solves the single-source shortest-paths problem in the general case in which edge weights may be negative. Given a weighted, directed graph G = (V, E) with source s and weight function w : E  R, the Bellman-Ford algorithm returns a boolean value indicating whether or not there is a negative-weight cycle that is reachable from the source. If there is such a cycle, the algorithm indicates that no solution exists. If there is no such cycle, the algorithm produces the shortest paths and their weights.

The algorithm uses relaxation, progressively decreasing an estimate d[v] on the weight of a shortest path from the source s to each vertex v  V until it achieves the actual shortest-path weight δ(s, v). The algorithm returns TRUE if and only if the graph contains no negative-weight cycles that are reachable from the source.

BELLMAN-FORD(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  for i1 to |V[G]| - 1
3       do for each edge (u, v) ∈ E[G]
4              do RELAX(u, v, w)
5  for each edge (u, v) ∈ E[G]
6       do if d[v] > d[u] + w(u, v)
7             then return FALSE
8  return TRUE

Figure 24.4 shows the execution of the Bellman-Ford algorithm on a graph with 5 vertices. After initializing the dand π values of all vertices in line 1, the algorithm makes |V| – 1 passes over the edges of the graph. Each pass is one iteration of the for loop of lines 2-4 and consists of relaxing each edge of the graph once. Figures 24.4(b)-(e) show the state of the algorithm after each of the four passes over the edges. After making |V|- 1 passes, lines 5-8 check for a negative-weight cycle and return the appropriate boolean value. (We’ll see a little later why this check works.)

(单击图片可以放大)

Figure 24.4: The execution of the Bellman-Ford algorithm. The source is vertex s. The d values are shown within the vertices, and shaded edges indicate predecessor values: if edge (u, v) is shaded, then π[v] = u. In this particular example, each pass relaxes the edges in the order (t, x), (t, y), (t, z), (x, t), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y). (a) The situation just before the first pass over the edges. (b)-(e) The situation after each successive pass over the edges. The d and π values in part (e) are the final values. The Bellman-Ford algorithm returns TRUE in this example.

The Bellman-Ford algorithm runs in time O(V E), since the initialization in line 1 takes Θ(V) time, each of the |V| – 1 passes over the edges in lines 2-4 takes Θ(E) time, and the for loop of lines 5-7 takes O(E) time.

以下是Bellman-Ford代码:

/*
* About:  Bellman-Ford算法
* Author: Tanky Woo
* Blog:   www.WuTianqi.com
*/
 
#include 
<iostream>
using namespace std;
const int maxnum = 100;
const int maxint = 99999;
 
// 边,
typedef struct Edge{
    
int u, v;    // 起点,重点
    int weight;  // 边的权值
}Edge;
 
Edge edge[maxnum];     
// 保存边的值
int  dist[maxnum];     // 结点到源点最小距离
 
int nodenum, edgenum, source;    // 结点数,边数,源点
 
// 初始化图
void init()
{
    
// 输入结点数,边数,源点
    cin >> nodenum >> edgenum >> source;
    
for(int i=1; i<=nodenum; ++i)
        dist[i] 
= maxint;
    dist[source] 
= 0;
    
for(int i=1; i<=edgenum; ++i)
    {
        cin 
>> edge[i].u >> edge[i].v >> edge[i].weight;
        
if(edge[i].u == source)          //注意这里设置初始情况
            dist[edge[i].v] = edge[i].weight;
    }
}
 
// 松弛计算
void relax(int u, int v, int weight)
{
    
if(dist[v] > dist[u] + weight)
        dist[v] 
= dist[u] + weight;
}
 
bool Bellman_Ford()
{
    
for(int i=1; i<=nodenum-1++i)
        
for(int j=1; j<=edgenum; ++j)
            relax(edge[j].u, edge[j].v, edge[j].weight);
    
bool flag = 1;
    
// 判断是否有负环路
    for(int i=1; i<=edgenum; ++i)
        
if(dist[edge[i].v] > dist[edge[i].u] + edge[i].weight)
        {
            flag 
= 0;
            
break;
        }
    
return flag;
}
int main()
{
    
//freopen("input3.txt", "r", stdin);
    init();
    
if(Bellman_Ford())
        
for(int i = 1 ;i <= nodenum; i++)
            cout 
<< dist[i] << endl;
    
return 0;
}
posted @ 2011-01-17 20:48 Tanky Woo 阅读(4540) | 评论 (2)编辑 收藏

已出连载

1.《随机化算法(1) — 随机数》

2.《随机化算法(2) — 数值概率算法》

3.《随机化算法(3) — 舍伍德(Sherwood)算法》

正文:

悟性不够,这一章看代码看了快一个上午,才理解。

上一章讲过《概率算法(3) — 舍伍德(Sherwood)算法》,但是他的有点是计算时间复杂性对所有实例而言相对均匀,而其平均时间复杂性没有改变。而拉斯维加斯算法怎么显著改进了算法的有效性。

拉斯维加斯算法的一个显著特征是它所作的随机性决策有可能导致算法找不到所需的解。因此通常用一个bool型函数表示拉斯维加斯算法。

void Obstinate(InputType x, OutputType &y)
{
 
    
// 反复调用拉斯维加斯算法LV(x, y),直到找到问题的一个解
    bool success= false;
    
while (!success) 
         success 
= LV(x,y);
}

 

 

 考虑用拉斯维加斯算法解决N皇后问题

对于n后问题的任何一个解而言,每一个皇后在棋盘上的位置无任何规律,不具有系统性,而更象是随机放置的。由此容易想到下面的拉斯维加斯算法。
在棋盘上相继的各行中随机地放置皇后,并注意使新放置的皇后与已放置的皇后互不攻击,直至n个皇后已相容地放置好,或已没有下一个皇后的可放置位置时为止。注意这里解决的是找到其中一个方法,求不是求出N皇后的全部解。

这里提前说明一下,否则不好理解。

接下来的这个用Las Vegas算法解决N皇后问题,我们采用的是随机放置位置策略回溯法相结合,具体就是比如八皇后中,前几行选择用随机法放置皇后,剩下的选择用回溯法解决。

这个程序不是很好理解,有的地方我特别说明了是理解程序的关键,大家看时一定要认真了,另外,王晓东的书上先是用单纯的随机法解决,大家可以先去理解书上这个例子。然后再来分析我这个程序。不过那本书上关于这一块错误比较多,大家看时要注意哪些地方他写错了。

拉斯维加斯算法解决N皇后代码:

依然用到了RandomNumber.h头文件,大家可以去看下第一章,我就不贴出来了。

剩下部分的代码:

注意QueensLV()函数是这个程序的精髓所在。

Queen.h头文件

#ifndef QUEEN_H
#define QUEEN_H
 
class Queen
{
    friend 
bool nQueen(int);
private:
    
bool Place(int k);             // 测试皇后k置于第x[k]列的合法性
    bool Backtrack(int t);         // 解n后问题的回溯法
    bool QueensLV(int stopVegas);  // 随机放置n个皇后拉斯维加斯算法
    int n, *x, *y;
};
 
#endif

 

 Queen.cpp文件

 #include <iostream>

#include "Queen.h"
#include 
"RandomNumber.h"
using namespace std;
 
bool Queen::Place(int k)
{
    
// 测试皇后k置于第x[k]列的合法性
    for(int j = 1; j < k; ++ j)
        
if((abs(k-j) == abs(x[j]-x[k])) || (x[j]==x[k]))
            
return false;
    
return true;
}
 
bool Queen::Backtrack(int t)
{
    
// 解n后问题的回溯法
    if(t > n)
    {
        
for(int i=1; i<=n; ++i)
            y[i] 
= x[i];
        
return true;
    }
    
else
        
for(int i=1; i<=n; ++i)
        {
            x[t] 
= i;
            
if(Place(t) && Backtrack(t+1))
                
return true;
        }
        
return false;
}
 
/*
* QueensLV是整个Las Vegas解n皇后的精髓。而且这个函数不好理解,我在这里具体分析下。
* k是第k行,x[k]表示第k行的皇后放在x[k]列
* 下面这两点解析请认真看了,我个人就是卡在这里半天了,但是理解后很简单。
* 标号①处:这里是遍历第k行所有可以放置的列号,用y保存下来,并用count记录有多少个位置可以放置
* 标号②处:这里利用上面保存的可以放置的列,然后随机取其中一列来放置第k行的皇后。这就是Las Vegas的精髓!
*/
// Author: Tanky Woo
// Blog:    www.WuTianQi.com
bool Queen::QueensLV(int stopVegas)
{
    
// 随机放置n个皇后的拉斯维加斯算法
    RandomNumber rnd;    // 随机数产生器
    int k = 1;           // 下一个放置的皇后编号
    int count = 1;
    
// 1 <= stopVegas <= n 表示允许随机放置的皇后数
    while((k <= stopVegas) && (count > 0))
    {
        count 
= 0;
        
for(int i = 1; i<=n; ++i)      // ----------- ①
        {
            x[k] 
= i;
            
if(Place(k))
                y[count
++= i;
        }
        
if(count > 0)                   // -------------②
            x[k++= y[rnd.Random(count)];   // 随机位置
    }
    
return (count > 0);   // count > 0表示放置位置成功
}

 

 Main.cpp主文件:

/*
* Author: Tanky woo
* Blog:   www.WuTianQi.com
* Date:   2010.12.9
* 拉斯维加斯(Las Vegas)算法解决N皇后问题
* 代码来至王晓东《计算机算法设计与分析》
*/
#include 
"Queen.h"
#include 
"RandomNumber.h"
#include 
<iostream>
using namespace std;
 
bool nQueen(int n)
{
    
// 与回溯法结合的解n后问题的拉斯维加斯算法
    Queen X;
    
// 初始化X
    X.n = n;
    
int *= new int[n+1];
    
int *= new int[n+1];
    
for(int i=0; i<=n; ++i)
    {
        p[i] 
= 0;
        q[i] 
= 0;
    }
    X.y 
= q;
    X.x 
= p;
    
// 设置随机放置皇后的个数
    int stop = 8;
    
if(n > 15)
        stop 
= n-15;
    
bool found = false;
    
while(! X.QueensLV(stop));
    
// 算法的回溯搜索部分
    if(X.Backtrack(stop+1))
    {
        
for(int i=1; i<=n; ++i)
            cout 
<< p[i] << " ";
        found 
= true;
    }
    cout 
<< endl;
    delete [] p;
    delete [] q;
    
return found;
}
 
int main()
{
    nQueen(
8);
}

 

 在8皇后问题中:

1.stopVegas=0表示完全使用回溯法解决问题。因此一定可以得到一组解。

2.stopVegas=8表示完全实用随机法解决问题。因此一定可以得到一组解。

注意书上说解决8皇后问题时,列出了一个不同stopVegas值时,所对应的算法成功率,在stopVegas=8时,他写着是0.1293,我个人认为是错的。接下来我说下自己这么理解的原因:

首先,这个程序为何会造成有失败的情况,那就是因为在随机求出前stopVegas行成立时,不代表后面N-stopVegas行用回溯就可以成立,因为这不是一个彻底的回溯。它是从stopVegas+1行开始计算,回溯不成立最后返回时,也只停留在stopVegas。

而我们在完全随机时,那么它就是反复调用随机位置放置n个皇后的Las Vegas算法,直至放置成功。所以当stopVegas=8时,他的成功率也应该是100%。

另外在stopVegas=3时,成功率等于0.49,趋近于0.5,大家可以试试,基本上每运行两次会有一次回来结果。

如果上面我的理解有错,欢迎大家指出,我的博客(http://www.wutianqi.com/)。

下一篇我会写《随机化算法(5) — 蒙特卡罗(Monte Carlo)算法》。

Tanky Woo原创,欢迎转载,转载请附上链接,请不要私自删除文章内任何关于本博客的链接。

 

posted @ 2010-12-15 14:28 Tanky Woo 阅读(3144) | 评论 (0)编辑 收藏

关于编程的学习,大家肯定都知道,也是大家都说来说去的,就几句话:

1.多看书。

2.多看代码。

3.多敲代码。

这些我不想多说,也觉得没有多说的必要。

经常在CSDN上看到有人问“我学习C++一段时间了,该如何进阶?”,然后接着就是一大堆的人,重复这上面的三句话或者更多,我不是说这些方法是错的,我只是认为,这样没有点到本质,初学者喜欢依赖于书籍,他们看书了,他们也照着书敲了代码,但是他们就是感觉一直在基础的层面上打转,这是为何呢?

在C++里定义复制构造函数时,大家知道,一般对于类中含有指针的,要进行深复制,而不是浅复制。而我在这里也要讲一个类似的方法,那就是关于编程的浅学习与深学习的问题

大家在这里可以先试着想想自己平时是怎么学习编程的?遇到一个新函数、新概念,大家是看书?记住概念?看看代码?抑或是其他?

我根据个人的理解和经验,在没遇到一个新知识时,我把学习这个知识点的深度分为三个层次,依次深入:

①.看了书,看了代码。

②.在①的基础上,照着书把代码敲在电脑里运行了。

③.在②的基础上,自己根据自己的理解和脑海里的记忆,不看书,把代码敲在电脑上,并运行。

对于第①个层次,一般会发生在以下情况下:平时没学习,考前疯狂的看书,但是没时间敲代码,于是把书和代码都用学习概念的方法—->死记,这样,直接导致了考时忘光光,考后欲哭无泪。

对于第②个层次,大部分人应该都处于这种情况。大家平时学习时,是一种机械化的学习,也就是第②种层次所说的,照着书敲代码,这样虽然当时把程序运行出来了,很高兴,但是,如果我接着让你不看书,自己动手再敲一遍,有几个人可以敲出来?抑或是,我把题目要求改一改,让你们用这个新学到的方法做,有几个人可以做出来?

这就是第②种层次的弊病,网上很多人都建议,自己动手把代码敲在电脑上,但是我相信,他们的本意是让大家不看书,把代码敲上去,而不是只是简单的照着书敲代码。

对于第①种层次,根本谈不上是学习;而第②种层次和第③种层次,就是我在文章标题里所说的浅学习和深学习的区别。

我说了很多,可能有些人觉得是废话,只需要一两句就可以说清楚的。本文的目的,只是为了分析浅层次与深层次学习的区别,进而能自己去区别学习层次,虽然一两句话也可以说清楚,但是却无法印刻在读者的脑海里,更无法自己去形成这个概念,也就无法判断自己的学习是否到位。

最后,我像把文章用几句话总结一下:

1.学习编程,要完成三个步骤:

        ①:看书,看代码;

        ②:对照着书敲代码;

        ③:抛开书本,自己根据自己理解,去敲代码,或者自己给个题目,然后用新学到的知识去解决;

2.学习编程,如果只做到上面两个层次,不如不学,把时间留着去打会球,因为这样根本没学到知识,当然,不排除有些人记忆力超强。

3.以上学习方法可以运用到其他学习上去。大家自行去理解,寻找一套适合自己的学习方法。

Tanky Woo原创,欢迎转载,转载请注明作者信息以及本博客:http://www.cppblog.com/tanky-woo/

Tanky Woo 标签: 
posted @ 2010-12-13 16:11 Tanky Woo 阅读(2577) | 评论 (11)编辑 收藏
     摘要: 已出连载: 1.《随机化算法(1) — 随机数》 2.《随机化算法(2) — 数值概率算法》 正文: 这一章怎么说呢,我个人感觉不好理解,在网上查了一些资料,没发现有具体对舍伍德算法的介绍。 迄今为止看的最全面的就是王晓东的《计算机算法设计与分析》里讲的了。在网上查的一些资料也基本全和这本书上讲的一样,至于是这本书先写的,还是其他位置先写的,我就不做评论了。 (有时间我把那本书上讲舍伍...  阅读全文
posted @ 2010-12-13 10:04 Tanky Woo 阅读(2788) | 评论 (0)编辑 收藏

接着上一篇: 随机化算法(1) — 随机数

在这章开篇推荐下chinazhangjie总结的随机算法,因为咱两看的是同一本书,所以大家也可以去参考下他的,总结的很不错。

http://www.cnblogs.com/chinazhangjie/archive/2010/11/11/1874924.html

(顺便再PS一下,小杰也是我论坛的C/C++问题求助板块的版主,C/C++小牛)

这一章我就把书中的一个例子举出来了,感觉虽然很简单,但是很有意思。

用随机投点法计算Pi值

设有一半径为r的圆及其外切四边形。向该正方形随机地投掷n个点。设落入圆内的点数为k。由于所投入的点在正方形上均匀分布,因而所投入的点落入圆内的概率为(Pi*r*r)/(4*r*r)= Pi/4 。所以当n足够大时,k与n之比就逼近这一概率。从而,PI 约等于 (4*k)/n.

如下图:

 

 因为代码里用到了上一章《概率算法(1) — 随机数》里的RandomNumber类,所以大家可以先把前一章看看。

我把这个伪随机类再贴一遍:

 const unsigned long maxshort = 65535L;

const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
 
class RandomNumber{
private:
    
// 当前种子
    unsigned long randSeed;
public:
    
// 构造函数,默认值0表示由系统自动产生种子
    RandomNumber(unsigned long s = 0);
    
// 产生0 ~ n-1之间的随机整数
    unsigned short Random(unsigned long n);
    
// 产生[0, 1) 之间的随机实数
    double fRandom();
};
 
// 产生种子
RandomNumber::RandomNumber(unsigned long s)
{
    
if(s == 0)
        randSeed 
= time(0);    //用系统时间产生种子
    else
        randSeed 
= s;
}
 
// 产生0 ~ n-1 之间的随机整数
unsigned short RandomNumber::Random(unsigned long n)
{
    randSeed 
= multiplier * randSeed + adder;
    
return (unsigned short)((randSeed >> 16% n);
}
 
// 产生[0, 1)之间的随机实数
double RandomNumber::fRandom()
{
    
return Random(maxshort) / double(maxshort);
}

 主文件Main:

/*
* Author: Tanky woo
* Blog:   www.WuTianQi.com
* Date:   2010.12.8
* 用随机投点法计算Pi值
* 代码来至王晓东《计算机算法设计与分析》
*/
 
#include 
"RandomNumber.h"
#include 
<iostream>
#include 
<iomanip>
#include 
<time.h>
using namespace std;
 
double Darts(long n)
{
    
// 用随机投点法计算Pi值
    static RandomNumber dart;
    
long k = 0;
    
for(long i=1; i<=n; ++i)
    {
        
double x = dart.fRandom();
        
double y = dart.fRandom();
        
// 在圆内
        if((x*x+y*y) <= 1)
            
++k;
    }
    
return 4 * k / double(n);
}
 
int main()
{
    
// 当进行1,000次投点时
    cout << Darts(1000<< endl;
    
// 当进行10,000次投点时
    cout << Darts(10000<< endl;
    
// 当进行100,000次投点时
    cout << Darts(100000<< endl;
    
// 当进行1,000,000次投点时
    cout << Darts(1000000<< endl;
    
// 当进行10,000,000次投点时
    cout << Darts(10000000<< endl;
    
// 当进行100,000,000次投点时
    cout << Darts(100000000<< endl;
    
return 0;
 
}

 

 通过代码可以看出,随机投点越多,则越接近与3.1415926…

这个和抛硬币时求硬币正反面概率类似,实验次数越多,则越接近于理论值。

下一章是《随机化算法(3) — 舍伍德(Sherwood)算法》

Tanky Woo原创,欢迎转载,转载请附上链接,请不要私自删除文章内任何关于本博客的链接。

posted @ 2010-12-12 11:51 Tanky Woo 阅读(2454) | 评论 (1)编辑 收藏

最近在看王晓东的《计算机算法设计与分析(第3版) 》,感觉讲的挺不错的。这里先推荐下。

接下来的几章(包括本章),我准备以连载的方式讲出来,主要用到的资料是上面推荐的那本书以及《算法导论》和网上的资源,内容是概率分析与随机算法。文章内大部分内容出自书中,我仅以汇总形式以及个人理解加以补充。如有纰漏,欢迎指出。

概率算法的一个基本特征是对所求解问题的同一实例用同一概率算法求解两次可能得到完全不同的效果。这两次求解问题所需的时间甚至所得到的结果可能会有相当大的差别。一般情况下,可将概率算法大致分为四类:数值概率算法,蒙特卡罗(Monte Carlo)算法,拉斯维加斯(Las Vegas)算法和舍伍德(Sherwood)算法。

随机数在概率算法设计中扮演着十分重要的角色。在现实计算机上无法产生真正的随机数,因此在概率算法中使用的随机数都是一定程度上随机的,即伪随机数

产生随机数最常用的方法是线性同余法。由线性同余法产生的随机序列a1,a2,…,an满足
1.a0=d
2.an=(b*an-1+c)mod m (n=1,2…….)
其中,b>0, c>=0, d>=m。d称为该随机序列的种子

一般情况下,取gcd(m, b)=1,因此可取b为一素数。

这是一个随机数类

const unsigned long maxshort = 65535L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
 
class RandomNumber{
private:
    
// 当前种子
    unsigned long randSeed;
public:
    
// 构造函数,默认值0表示由系统自动产生种子
    RandomNumber(unsigned long s = 0);
    
// 产生0 ~ n-1之间的随机整数
    unsigned short Random(unsigned long n);
    
// 产生[0, 1) 之间的随机实数
    double fRandom();
};
 
// 产生种子
RandomNumber::RandomNumber(unsigned long s)
{
    
if(s == 0)
        randSeed 
= time(0);    //用系统时间产生种子
    else
        randSeed 
= s;
}
 
// 产生0 ~ n-1 之间的随机整数
unsigned short RandomNumber::Random(unsigned long n)
{
    randSeed 
= multiplier * randSeed + adder;
    
return (unsigned short)((randSeed >> 16% n);
}
 
// 产生[0, 1)之间的随机实数
double RandomNumber::fRandom()
{
    
return Random(maxshort) / double(maxshort);
}

利用这个随机数类,写一个程序,模拟抛硬币的实验。

抛10次硬币构成一个事件,每次事件记录得到正面的个数。反复模拟这个事件50,000次,然后对这50,000L次进行输出频率图,比较每次事件得到正面次数的比例。

以下是总的代码:
头文件 RandomNumber.h:

// RandomNumber.h
 
const unsigned long maxshort = 65535L;
const unsigned long multiplier = 1194211693L;
const unsigned long adder = 12345L;
 
#ifndef RANDOMNUMBER_H
#define RANDOMNUMBER_H
 
class RandomNumber{
private:
    
// 当前种子
    unsigned long randSeed;
public:
    
// 构造函数,默认值0表示由系统自动产生种子
    RandomNumber(unsigned long s = 0);
    
// 产生0 ~ n-1之间的随机整数
    unsigned short Random(unsigned long n);
    
// 产生[0, 1) 之间的随机实数
    double fRandom();
};
 
#endif

类实现文件RandomNumber.cpp :

// RandomNumber.cpp
#include "RandomNumber.h"
#include 
<iostream>
#include 
<stdlib.h>
#include 
<time.h>
 
using namespace std;
 
// 产生种子
RandomNumber::RandomNumber(unsigned long s)
{
    
if(s == 0)
        randSeed 
= time(0);    //用系统时间产生种子
    else
        randSeed 
= s;
}
 
// 产生0 ~ n-1 之间的随机整数
unsigned short RandomNumber::Random(unsigned long n)
{
    randSeed 
= multiplier * randSeed + adder;
    
return (unsigned short)((randSeed >> 16% n);
}
 
// 产生[0, 1)之间的随机实数
double RandomNumber::fRandom()
{
    
return Random(maxshort) / double(maxshort);
}

主文件Main :
// 主文件main
/*

* Author: Tanky woo
* Blog:   www.WuTianQi.com
* Date:   2010.12.7
* 代码来至王晓东《计算机算法设计与分析》
*/
#include 
"RandomNumber.h"
#include 
<iostream>
#include 
<iomanip>
#include 
<time.h>
using namespace std;
 
int TossCoins(int
 numberCoins)
{
    
// 随机抛硬币

    static RandomNumber coinToss;
    
int i, tosses = 0
;
    
for(i = 0; i < numberCoins; ++
i)
        tosses 
+= coinToss.Random(2
);
    
return
 tosses;
}
 
int
 main()
{
    
// 模拟随机抛硬币事件

    const int NCOINS = 10;
    
const long NTOSSES = 50000L
;
    
// heads[i]得到的i次正面的次数

    long i, heads[NCOINS+1];
    
int
 j, position;
    
// 初始化数组heads

    for(j = 0; j < NCOINS+1++j)
        heads[j] 
= 0
;
    
// 重复50,000次模拟事件

    for(i = 0; i < NTOSSES; ++i)
        heads[TossCoins(NCOINS)] 
++
;
    
// 输出频率图

    for(i = 0; i <= NCOINS; ++i)
    {
        position 
= int (float(heads[i]) / NTOSSES*72
);
        cout 
<< setw(6<< i << " "
;
        
for(j = 0; j<position-1++
j)
            cout 
<< " "
;
        cout 
<< "*" <<
 endl;
    }
    
return 0
;
}

输出频率图:

具体可以看看王晓东的《计算机算法设计与分析》第七章。

下一篇我会写《随机化算法(2) — 数值概率算法》。

 Tanky Woo原创,欢迎转载,转载请附上链接,请不要私自删除文章内任何关于本博客的链接。

posted @ 2010-12-10 08:01 Tanky Woo 阅读(9355) | 评论 (2)编辑 收藏

虽然这个问题已经在网上被讨论遍了,但是最近从新拾起算法,感觉有必要夯实一下基础。

棋盘覆盖问题
首先大致描述一下题目
在一个2^k×2^k个方格组成的棋盘中,若有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一个特殊棋盘.显然特殊方格在棋盘上出现的位置有4^k种情形.因而对任何
k≥0,有4^k种不同的特殊棋盘.
下图–图(1)中的特殊棋盘是当k=2时16个特殊棋盘中的一个:

 

图(1)

题目要求在棋盘覆盖问题中,要用下图—图(2)所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖.

 

图(2)

思路分析:
当k>0时,将2^k×2^k棋盘分割为4个2^k-1×2^k-1子棋盘,如下图–图(3)所示:


 

图(3)

特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格.为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处。
如下图–图(4)所示,这3个子棋盘上被L型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而原问题转化为4个较小规模的棋盘覆盖问题.递归地使用这种分割,直至棋盘简化为1×1棋盘。

以下是代码:

 

/*
Author: Tanky Woo
Blog:   www.WuTianQi.com
棋盘覆盖问题
分治法
2010-12-3
*/
#include 
<iostream>
using namespace std;
const int N = 11;
int Board[N][N];
int tile = 0;
 
/*
tr:棋盘左上角方格的行号
tc:棋盘左上角方格的列号
dr:特殊方格所在的行号
dc:特殊方格所在的列号
size:方形棋盘的边长
*/
void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
    
if(size == 1)
        
return;
    
int t = ++tile, s = size/2;
 
    
//覆盖左上角子棋盘
    if(dr<tr+&& dc<tc+s)
        
//特殊方格在此棋盘中
        ChessBoard(tr, tc, dr, dc, s);
    
else   // 此棋盘无特殊方格
    {
        
// 用t号L型骨型牌覆盖右下角
        Board[tr+s-1][tc+s-1= t;
        
// 覆盖其余方格
        ChessBoard(tr, tc, tr+s-1, tc+s-1, s);
    }
 
    
//覆盖右上角子棋盘
    if(dr<tr+&& dc>=tc+s)
        ChessBoard(tr, tc
+s, dr, dc, s);
    
else
    {
        Board[tr
+s-1][tc+s] = t;
        ChessBoard(tr, tc
+s, tr+s-1, tc+s, s);
    }
 
    
//覆盖左下角子棋盘
    if(dr>=tr+&& dc<tc+s)
        ChessBoard(tr
+s, tc, dr, dc, s);
    
else
    {
        Board[tr
+s][tc+s-1= t;
        ChessBoard(tr
+s, tc, tr+s, tc+s-1, s);
    }
 
    
//覆盖右下角子棋盘
    if(dr>=tr+&& dc>=tc+s)
        ChessBoard(tr
+s, tc+s, dr, dc, s);
    
else
    {
        Board[tr
+s][tc+s] = t;
        ChessBoard(tr
+s, tc+s, tr+s, tc+s, s);
    }
}
 
void DisplayBoard(int size)
{
    
for(int i=1; i<=size; ++i)
    {
        
for(int j=1; j<=size; ++j)
            printf(
"%2d ", Board[i][j]);
        printf(
"\n");
    }
}
 
int main()
{
    ChessBoard(
11124);
    DisplayBoard(
4);
    
return 0;
}
posted @ 2010-12-08 10:23 Tanky Woo 阅读(3878) | 评论 (7)编辑 收藏
仅列出标题
共7页: 1 2 3 4 5 6 7