【算法编程】过河问题

今天偶尔想到了过河问题。记得读小学六年级的时候第一次接触到这个问题--六个老虎过河问题(百度上有详细介绍,本文解决的是一个简单的问题,下一篇文章中将讨论该问题),当时都是从逻辑思维的方法得到正确的解决方法。本文介绍了普遍适用该类问题的方法以及该方法的改进方法,下一篇文章将介绍问题的变型及解法。

向量法(人、狗、鸡、米过河问题)

问题描述:某人带狗、鸡、米用船来过河,只有人会划船(好像是废话,后面问题我们还会假设动物也会划船),另外至多还能载一物,当人不在时,狗要吃鸡(有人可能会质疑:狗吃鸡?,但是我看到的是狗和猫都吃小鸡),鸡吃米。问人、狗、鸡、米怎么过河?

我们用一个向量来表示人、狗、鸡、米所处的状态,例如:(1 1
1 1)表示人、狗、鸡、米都在左岸,则对应的(0 0 0 0)表示人、狗、鸡、米都在右岸。这些向量我们称为状态向量,但是由于问题的条件限制,有些状态是允许的,而有些状态是不允许的,例如(0
1 1 1)表示人不在左岸,显然是不允许的。我们可以穷举出所有允许的状态:

(1 1 1 1)     
  (0 0 0 0)

(1 1 1 0)     
  (0 0 0 1)

(1 1 0 1)     
  (0 0 1 0)

(1 0 1 1)     
  (0 1 0 0)

(1 0 1 0)     
  (0 1 0 1)

从上面的允许状态中,我们可以发现规律如下:

当人在时(也就是第一位为1时),不能有相邻的0,例如(1 1 0 0)是不允许的

当人不在时(也就是第一个为0时),不能有相邻的1 ,例如(0 1 1 0)是不允许的

我们将船的一次运载也用向量表示,例如(1 1 0 0)表示人和狗在船上。由于只有人会划船,则允许的运算向量为:

(1 1 0 0)     
  (1 0 1 0)        (1 0 0 1) 
      (1 0 0 0)

因此我们可以将一次过河过程看成是一个状态向量与一个运算向量的异或运算(模2加运算:1+1=0 1+0=1 0+0=0)。根据上述的向量法的描述,我们可以将问题简化成:将状态(1 1 1 1)经过奇数次与运算向量运算,变成状态为(0 0 0 0)的状态转移过程。下面是过河的图解过程

开始状态     
          船上状态     
           结果状态

1       (1
1 1 1)    ------>   
(1 0 1 0)     ------>  
  (0 1 0 1)

2       (0 1 0 1)    ------> 
  (1 0 0 0)     ------> 
   (1 1 0 1)

3       (1 1 0 1)    ------> 
  (1 0 0 1)     ------>    
(0 1 0 0)

4       (0 1 0 0) 
  ------>    (1
0 1 0)     ------>     (1 1 1 0)

5       (1 1 1 0) 
  ------>    (1
1 0 0)     ------>     (0
0 1 0)

6       (0 0 1 0) 
  ------>    (1
0 0 0)     ------>     (1 0 1 0)

7       (1 0 1 0) 
  ------>    (1
0 1 0)     ------>   
 (0 0 0 0)

奇数次:去河对岸

偶数次:回河这边

注意事项:

在第3次过河时,开始状态为(1 1 0 1),如果船上状态为(1 1 0 0),则结果状态为(0 0 0 1),然后经过船上状态(1 0 0 1),结果状态为(1 0 0 0),然后经过船上状态(1 0 0 0),就可以完成任务(总共5次过河)。但是这里存在问题:当开始状态为(0
0 0 1),船上状态不可能为(1 0 0 1)。因为开始状态(0 0 0 1)表示只有米在左岸,船上状态(1 0 0 1)表示人和米在船上,这是不可能的!因此船上状态的选择是有限制的。奇数时,开始状态为1的位置,船上对应位置才可以为1;偶数时,开始状态为0的位置,船上对应的位置才可以为0.通俗的说:奇数时,是将有的东西运到河对岸,偶数时,是将河对岸的东西(河这边没有)运到河这边。这些数学的表述可能太麻烦,我举例说明:奇数时,当河这边只有人、狗、米,我们可以从选择人、狗上船或则人、米上船,而不能选择人、鸡上船(鸡在对岸);当偶数次数时,河这边是狗、河对岸则是人、鸡、米,我们可以人、鸡或则人、米回到河这边,而不能选择人、狗过河。

算法实现:

上面的实现可用matlab或则c来实现。若用matlab来实现,则那些状态向量以及状态间的异或运算比较容易表示;若用c来实现,则用时较短。两者的难点在于注意事项中的船上变量的选取问题。因此这种方法不适合用计算机实现,在状态变量较少的情况下,我们可以直接用手工进行运算的方法来得到结果(大家可以试试)。

改进型算法---图论法

算法思路:将10个状态向量用10个点表示,将这10个状态向量分别与可行的运算向量进行运算,如果结果向量仍为允许的状态向量,则两者间连一条线,从而构成了一个图的问题。我们的目标是找到一条可以从状态(1 1 1 1)到状态(0 0 0 0)的通路。下面是我运算得到的图:

注意:图中的标号用于表示对应的状态

具体算法实现如下:

1、Dijkstra算法

#include<stdio.h>
#define M 20//边数
#define N 10//顶点数
#define MAX 10000
void Dijkstra(int v, int dist[][N],int D[N],int p[N],int s[N]) ;
int flag[N]={0};
int flag1=0;
int flag2=0;
typedef struct
{
    int startvex;
    int endvex;
    int length;
}edge;//边的结构体
edge T[M];
void main()
{
    int dist[N][N]={{0,MAX,MAX,MAX,MAX,1,MAX,MAX,MAX,MAX},//图的邻接矩阵
                    {MAX,0,MAX,MAX,MAX,MAX,1,1,MAX,MAX},
                    {MAX,MAX,0,MAX,MAX,1,1,MAX,1,MAX},
                    {MAX,MAX,MAX,0,MAX,MAX,MAX,1,1,MAX},
                    {MAX,MAX,MAX,MAX,0,MAX,MAX,1,MAX,1},
                    {1,MAX,1,MAX,MAX,0,MAX,MAX,MAX,MAX},
                    {MAX,1,1,MAX,MAX,MAX,0,MAX,MAX,MAX},
                    {MAX,1,MAX,1,1,MAX,MAX,0,MAX,MAX},
                    {MAX,MAX,1,1,MAX,MAX,MAX,MAX,0,MAX},
                    {MAX,MAX,MAX,MAX,1,MAX,MAX,MAX,MAX,0}
    };
    int D[N]={0};
    int p[N]={0};
    int s[N]={0};
    int num=0;
    Dijkstra(0,dist,D, p,s) ;//0表示从状态(1111)开始
}
 void Dijkstra(int v, int dist[][N],int D[N],int p[N],int s[N])
 {     int i, j, k, v1, min, max=10000, pre;     /* Max中的值用以表示dist矩阵中的值?*/
    v1=v;
    for( i=0; i<N; i++)              /* 各数组进行初始化*/
    {    D[i]=dist[v1][i];
        if( D[i] != MAX )  p[i]= v1+1;
        else p[i]=0;
        s[i]=0;
    }
    s[v1]=1;                          /* 将源点送U */
      for( i=0; i<N-1; i++)      /* 求源点到其余顶点的最短距离*/
    {    min=10001;    /* min>max, 以保证值为?的的的的顶顶顶顶点点点点也也也也能能能能加加加加入入入入U */
        for( j=0; j<N-1; j++)
              if ( ( !s[j] )&&(D[j]<min) )          /* 找出到源点具有最短距离的边*/
                  {min=D[j];
                        k=j;
                     }
                s[k]=1;  /* 将找到的顶点k送入U */
    for(j=0; j<N; j++)
     if ( (!s[j])&&(D[j]>D[k]+dist[k][j]) ) /* 调整V-U中各顶点的距离值*/
        {D[j]=D[k]+dist[k][j];
        p[j]=k+1;                      /* k是j的前趋*/
                }
            }                               /*  所有顶点已扩充到U中*/
            for( i=0; i<N; i++)
            {
                printf(" %d : %d ", D[i], i);
                pre=p[i];
            while ((pre!=0)&&(pre!=v+1))
            {    printf ("<- %d ", pre-1);
                pre=p[pre-1];
            }
            printf("<-%d \n", v);
        }
}

结果显示如下:

从上图的第七行可知,从标号为1的状态到标号为10的状态所要经过的过程为(数组下标是从0开始的):

    1---6---3---7---2---8---5---10

2、通过每对顶点之间的最短路径算法实现:

#include<stdio.h>
#define N 10 //顶点个数
#define MAX 10000
void Floyd(int dist[N][N],int A[N][N],int path[N][N])
{
    for(int i=0;i<N;i++)
        for(int j=0;j<N;j++)
            for(int k=0;k<N;k++)
            {
                /*if(A[i][j]>(A[i][k]+dist[k][j]))//方法一:计算每一次矩阵
                {
                    A[i][j]=(A[i][k]+dist[k][j]);
                    path[i][j]=path[k][j];
                }*/
                if(A[i][j]>(A[i][k]+A[k][j]))//方法二:计算的幂次矩阵
                {
                    A[i][j]=(A[i][k]+A[k][j]);
                    path[i][j]=path[k][j];
                }
            }
}
void main()
{
    int dist[N][N]={{0,MAX,MAX,MAX,MAX,1,MAX,MAX,MAX,MAX},//图的邻接矩阵
                    {MAX,0,MAX,MAX,MAX,MAX,1,1,MAX,MAX},
                    {MAX,MAX,0,MAX,MAX,1,1,MAX,1,MAX},
                    {MAX,MAX,MAX,0,MAX,MAX,MAX,1,1,MAX},
                    {MAX,MAX,MAX,MAX,0,MAX,MAX,1,MAX,1},
                    {1,MAX,1,MAX,MAX,0,MAX,MAX,MAX,MAX},
                    {MAX,1,1,MAX,MAX,MAX,0,MAX,MAX,MAX},
                    {MAX,1,MAX,1,1,MAX,MAX,0,MAX,MAX},
                    {MAX,MAX,1,1,MAX,MAX,MAX,MAX,0,MAX},
                    {MAX,MAX,MAX,MAX,1,MAX,MAX,MAX,MAX,0}
    };
    int A[N][N];
    int path[N][N]={0};//给出两顶点间的路径
    int pre=0;
    for(int i=0;i<N;i++)
        for(int j=0;j<N;j++)
        {
            A[i][j]=dist[i][j];
            if(dist[i][j]!=MAX)
                path[i][j]=i+1;
            else
                path[i][j]=0;
        }

    for(int k=0;k<7;k++)//若用方法一,需循环N-3次,若用方法二,需要循环lg(N-1)次
        Floyd(dist,A,path);
    printf("每对顶点间的最短路径矩阵为:\n");
    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
            printf("%d ",A[i][j]);
        printf("\n");
    }
    printf("\n每对顶点的具体最短路径为:\n");

    for(int i=0;i<N;i++)
    {
        for(int j=0;j<N;j++)
        {
            printf("%d: %d ",A[i][j],j+1);
        pre=path[i][j];
        while((pre!=0)&&(pre!=i+1))
        {
            printf("<- %d ",pre);
            pre=path[i][pre-1];
        }
        printf(" <- %d\n",i+1);
        }
    }
}

结果显示如下:

从上图的最短路径矩阵的第一行第10列可知,从状态1到状态10需要7步,从具体最短路径的第10行可知,所要经过的过程为:

 1---6---3---7---2---8---5---10

两种方法求得的结果相同,我们可以用图形象的表示如下:

通过对比可以发现,图论法实质是在向量法的基础上进行改进的算法,无论是在手动计算还是计算机实现上都比向量法更好。

【算法编程】过河问题,布布扣,bubuko.com

时间: 2024-10-13 12:37:45

【算法编程】过河问题的相关文章

一维向量旋转算法 编程珠玑 第二章

看了编程珠玑第二章,这里面讲了三道题目,这里说一下第二题,一维向量旋转算法. 题目:将一个n元一维向量(例数组)向左旋转i个位置. 解决方法:书上讲解了5种方法,自己只想起来2种最简单方法(下面讲的前两种). 1.原始方法. 从左向右依次移动一位,对所有数据平移:这样循环i次,算法最坏时间复杂度达n^2.耗时不推荐. 2.空间换时间. 顾名思义,申请一个i长度的空间,把前i半部分放到申请空间中,再把后面的所有数据向左移动i个位置,最后把申请的空间中的数据放到后半部分.浪费空间,不推荐. 3.杂技

LeetCode算法编程(两题)

今天看到酷壳推荐的国外编程LeetCode算法编程网站,上面目前有154道算法题,感觉很有意思,平常工作也比较忙,现在很少有时间来锻炼算法相关的东西,有空的时候静下心来,温习下基础,活跃下自已的思路,也是有必要的.先做了几道,后面会陆续补充其它的题目. 1.题目-PlusOne Given a non-negative number represented as an array of digits, plus one to the number. The digits are stored s

【算法编程】循环右移一个数组

仅用一个辅助节点将一个大小为n数组循环右移k位的三种办法: 1.时间复杂度最大:将所有元素每次只移动一位,总共移动k次,程序实现十分容易,在此就不具体实现了. 2.时间复杂度适中:依次将每个元素都放到辅助节点上,然后将其储存到目的节点,具体程序如下: #include<iostream> using namespace std; int gcd(int x,int y); int main() { int n,k; cout<<"请输入数组的维数"<<

阶乘求和与冒泡算法编程

编程题是写1!+2!+...+10!: 冒泡算法编程

【算法编程】小学数学题难倒博士

昨天在科学网上得知这样一个新闻<越南小学数学题难倒博士>,据悉题目来自越南保禄小学三年班,不过报道称该题难倒了上至博士下至家长,未免也太言过其实了. 题目描述 学生需要在下图表格中按由上至下.从左到右的顺序,填入1~9的数字,可重复填写,并按先乘除后加减(图中冒号代表除法)的运算法则,完成整条算式. 解题方法 显然,这题对于我们这种程序员来说完全不是问题,只要在大一上过C语言的学生(我们学校全校都学过C,即使是文科专业)基本上都可以用九重for循环来穷举解出此题,下面我分别用C和Matlab实

算法编程学习之递归

递归:程序调用自身的编程技巧称为递归( recursion).递归做为一种算法在程序设计语言中广泛应用. 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量.递归的能力在于用有限的语句来定义对象的无限集合.一般来说,递归需要有边界条件.递归前进段和递归返回段.当边界条件不满足时,递归前进:当边界条件满足时,递归返回. 实例:

【甘道夫】Mahout推荐算法编程实践

引言 Taste是曾经风靡一时的推荐算法框架,后来被并入Mahout中,Mahout的部分推荐算法基于Taste实现. 下文介绍基于Taste实现最常用的UserCF和ItemCF. 本文不涉及UserCF和ItemCF算法的介绍,这方面网上资料很多,本文仅介绍如何基于Mahout编程实现. 欢迎转载,请注明来源: http://blog.csdn.net/u010967382/article/details/39183839 步骤一:构建数据模型 UserCF和ItemCF算法的输入数据是用户

算法编程(一)

有大量(几千万条级别)的号码段(或单一号码)相应地址的数据.如 130123--130129  成都 1301241--1301250  重庆 13012510001       成都 -- 请编程实现 1.输入任一号码,查找相应的地址.如没有提示未找到 2.号段数据的动态添加和删除 3.说出自己算法的时间复杂度和空间复杂度

需掌握 - JAVA算法编程题50题及答案

[程序1] 题目:古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少? //这是一个菲波拉契数列问题public class lianxi01 {public static void main(String[] args) {System.out.println("第1个月的兔子对数: 1");System.out.println("第2个月的兔子对数: 1");int f1