将算法应用于实际——商人过河问题

611 阅读3分钟

Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情

商人过河问题

三名商人各带一个随从乘船渡河。现此岸有一小船只能容纳两人,由他们自己划行。若在河的任一岸随从人数比商人多,他们就可能抢劫财物。不过如何乘船渡河的大权由商人们掌握。商人们怎样才能安全过河呢?

如何建模呢?

注意到,我们可以把商人、随从已经河的位置抽象为一个状态。随着小船载人过河,状态也就随着发生了转移。那么问题就简化为了,有一系列状态,并且状态之间可以转移,那么能够从最开始的状态转移至最后的状态呢?

状态的定义

**如何定义状态?**注意到我们只需要得知商人和随从分别在左岸的数量,那么右岸的人数可以直接推断出来。另外在表示一下船的位置(在左岸还是右岸),我们即可准确唯一的表达一个状态。

定义如下三元组:

(m,f,b)(m,f,b)

其中 mm 是商人在左岸的数量, ff 是随从在左岸的数量, bb 是船的位置(为0在左岸,为1在右岸)。

从而此三元组就能表示唯一的一个状态。

此时问题转化为:

(3,3,0)...(0,0,1)(3,3,0) \to ... \to (0,0,1)

有没有可行的解?

有些状态是不可行的!

很遗憾,并不是所有0m30\le m\le 30f30\le f\le 3 的任意 m 和 f 都可以构成一个可行的状态。不然,就能很轻易的把所有人都运到对岸。

哪些状态是可行的?从题意可知,只有两岸的商人数都大于等于随从数,或者商人数为0,才是可行的状态。可以用3维数组able[m][f][b]表示该状态是否可行。具体的算法如下:

for (int m = 0; m <= mNum; m++)
   {
      for (int f = 0; f <= fNum; f++)
      {
         int m1 = mNum - m, f1 = fNum - f;
         //任何一边,商人比随从多或者商人为0为可行状态
         if ((m >= f || m == 0) && (m1 >= f1 || m1 == 0))
         {
            able[m][f][0] = able[m][f][1] = 1;
            printf("(%d %d %d) ", m, f, 0);
            printf("(%d %d %d) \n", m, f, 1);
            stateNum += 2;
         }
      }
   }

如何状态转移?

我们虽然知道,状态的转移其实就是用船从将人运来运去。我们可以手写出所有状态转移的可能性,在这里列出一种:

(3,3,0)(2,2,1)(3,3,0)\to(2,2,1)

但是既然我们要用计算机来解决问题,当然就不用自己手动枚举啦~~ 以下是我枚举状态转移的方式(纯纯暴力解法:

   for (int d1 = 0; d1 <= bCap; d1++)
   {
      for (int d2 = 0; d2 <= bCap; d2++)
      {
         if (d1 + d2 <= bCap && d1 + d2 > 0)
         {
            int m1, f1, b1;
            if (b == 1)
            {
               m1 = m + d1, f1 = f + d2, b1 = 0;
               if (m1 <= mNum && f1 <= fNum && able[m1][f1][b1] && !found[m1][f1][b1])
                  //此时说明(m,f,b)可以转移到(m1,f1,b1)
            }
            else
            {
               m1 = m - d1, f1 = f - d2, b1 = 1;
               if (m1 >= 0 && f1 >= 0 && able[m1][f1][b1] && !found[m1][f1][b1])
                  //此时说明(m,f,b)可以转移到(m1,f1,b1)
            }
         }
      }
   }

其中 d1d1d2d2 是表示船运向对岸几个商人和随从。

那么,知道了有哪些状态,和如何转移状态,如何求是否有解?

图论又来大显身手了~

显然我们可以把状态(m,f,t)抽象为结点,把状态的转移抽象为图的边,即如果状态(m,f,t)可以转移到状态(m1,f1,t1),那么就把图中的从前者向后者连一条边即可~~~

最后,我们应用DFS深度搜索算法从起点(3,3,0)开始搜索,如果搜索到结点(0,0,1),就说明有方式,如果没有就说明无解啦~~~

int DFS(int m, int f, int b)
{
   found[m][f][b] = 1;
   if (m == 0 && f == 0 && b == 1)
   {
      found[m][f][b] = 0;
      return 1;
   }
   int flag = 0;
   for (int d1 = 0; d1 <= bCap; d1++)
   {
      for (int d2 = 0; d2 <= bCap; d2++)
      {
         if (d1 + d2 <= bCap && d1 + d2 > 0)
         {
            int m1, f1, b1;
            if (b == 1)
            {
               m1 = m + d1, f1 = f + d2, b1 = 0;
               if (!(m1 <= mNum && f1 <= fNum && able[m1][f1][b1] && !found[m1][f1][b1]))
                  continue;
            }
            else
            {
               m1 = m - d1, f1 = f - d2, b1 = 1;
               if (!(m1 >= 0 && f1 >= 0 && able[m1][f1][b1] && !found[m1][f1][b1]))
                  continue;
            }
            if (DFS(m1, f1, b1))
               return 1;
         }
      }
   }
   return 0;
}

答案

最后的答案显然是无解啦~~~