第十五章 动态规划_思考题

  • Post author:
  • Post category:其他




概要

题目很多但是每题都很经典,有的题目确实是想了很久也没有一个合理的解答(可以说过程很痛苦),故有部分答案是参照别人的做法。有一部分是根据自己的理解来解题,不足之处会慢慢修改。



15.1 有向无环图中的最长简单路径

给定一个有向无环图G=(V,E),边权重为实数,给定图中两个顶点s和t。设计动态规划算法,求从s到t的最长加权简单路径。子问题图是怎么样的?算法的效率如何?



15.2 最长回文子序列

回文是正序和逆序列相同的非空字符串。例如,所有长度为1的字符串、civic、raqcecar、aibohphpbia都是回文。

设计高效算法,求给定输入字符串的最长回文子序列。例如,给定输入character,算法应该返回carac。算法的运行时间是怎样的?

两种思路,比较偷懒的做法是已知数组a,先把它的倒序用数组b保存,再求a和b的最长公共子序列。

还有一个思路如下:

先构建最优解的结构特征,m[i,j]表示从数组n的i序列出发到j序列的最长回文(0<=i<=j<=n)。

递归式如下:





m

[

i

,

j

]

=

{

1

若i=j

1

若i=j-1且a[i]!=a[j]

2

若i=j-1且a[i]=a[j]

m

a

x

(

m

[

i

+

1

,

j

]

,

m

[

i

,

j

1

]

)

若i<j-1且a[i]!=a[j]

m

[

i

+

1

,

j

1

]

+

2

若i<j-1且a[i]=a[j]

m[i,j]= \begin{cases} 1& \text{若i=j}\\ 1& \text{若i=j-1且a[i]!=a[j]}\\ 2& \text{若i=j-1且a[i]=a[j]}\\ max(m[i+1,j],m[i,j-1])& \text{若i<j-1且a[i]!=a[j]}\\ m[i+1,j-1] + 2& \text{若i<j-1且a[i]=a[j]}\\ \end{cases}






m


[


i


,




j


]




=













































































































































































1








1








2








m


a


x


(


m


[


i




+




1


,




j


]


,




m


[


i


,




j









1


]


)








m


[


i




+




1


,




j









1


]




+




2































i=j













i=j-1





a[i]!=a[j]













i=j-1





a[i]=a[j]













i<j-1





a[i]!=a[j]













i<j-1





a[i]=a[j]




























代码如下:

        static void Main(string[] args)
        {
            var text = "character";
            var charArray = text.ToArray();
            int[,] m = new int[charArray.Length, charArray.Length];
            string[,] s = new string[charArray.Length, charArray.Length];
            for (var i = 0; i < charArray.Length; i++)
            {
                m[i, i] = 1;
            }
            var r = Execute(charArray, m, s, 0, charArray.Length - 1);
            PrintPalindrome(charArray, s, r, 0, charArray.Length - 1);
        }

        /// <summary>
        /// 最长回文子序列(自顶向下带备忘录)
        /// </summary>
        /// <param name="ca">原字符串数组</param>
        /// <param name="m">长度存放数组</param>
        /// <param name="i">起点下标</param>
        /// <param name="j">终点下标</param>
        /// <returns></returns>
        public static int Execute(char[] ca, int[,] m, string[,] s, int i, int j)
        {
            if (i > j)
            {
                return 0;
            }
            if (m[i, j] > 0)
            {
                return m[i, j];
            }
            if (ca[i] != ca[j])
            {
                var r_1 = Execute(ca, m, s, i + 1, j);
                var r_2 = Execute(ca, m, s, i, j - 1);
                if (r_1 > r_2)
                {
                    s[i, j] = "->";
                    m[i, j] = r_1;
                    return r_1;
                }
                else
                {
                    s[i, j] = "<-";
                    m[i, j] = r_2;
                    return r_2;
                }
            }
            else
            {
                s[i, j] = "-><-";
                var r = Execute(ca, m, s, i + 1, j - 1) + 2;
                m[i, j] = r;
                return m[i, j];
            }
        }

        /// <summary>
        /// 打印回文
        /// </summary>
        /// <param name="ca">原字符串数组</param>
        /// <param name="s">辅助表</param>
        /// <param name="r">回文长度</param>
        /// <param name="i">起点下标</param>
        /// <param name="j">终点下标</param>
        public static void PrintPalindrome(char[] ca, string[,] s, int r, int i, int j)
        {
            char[] p = new char[r];
            var head = 0;
            var tail = p.Length - 1;
            while (i < j)
            {
                if (s[i, j] == "->")
                {
                    i++;
                }
                else if (s[i, j] == "<-")
                {
                    j--;
                }
                else if (s[i, j] == "-><-")
                {
                    p[head++] = ca[i++];
                    p[tail--] = ca[j--];
                }
            }
            if (i == j)
            {
                p[head] = ca[i];
            }
            for (var k = 0; k < p.Length; k++)
            {
                Console.Write(p[k]);
            }
        }



15.3 双向欧几里得旅行商问题

先扔代码上来,此题卡了很久,对于该题的分析在日后放出。

在这里插入图片描述

        static void Main(string[] args)
        {
            Console.WriteLine("请输入坐标个数");
            var count = Convert.ToInt32(Console.ReadLine());
            var points = new Point[count];
            for (var i = 0; i < count; i++)
            {
                Console.Write("请输入坐标" + (i + 1) + "的X值");
                var x = Convert.ToDouble(Console.ReadLine());
                Console.Write("请输入坐标" + (i + 1) + "的Y值");
                var y = Convert.ToDouble(Console.ReadLine());
                points[i] = new Point(x, y);
            }
            double[,] m = null;
            int[,] s = null;
            TSP_Length(points, ref m, ref s);
            PrintTSP(points.Length, m, s);
        }
        
        #region "计算两点之间的欧几里得距离"
        private static double GetDistance(Point[] points, int p1, int p2)
        {
            return Math.Sqrt((points[p1].X - points[p2].X) * (points[p1].X - points[p2].X)
                + (points[p1].Y - points[p2].Y) * (points[p1].Y - points[p2].Y));
        }
        #endregion

        #region '快速排序'
        public static void QuickSort(Point[] points,
                            int p, int r)
        {
            if (p >= r)
            {
                return;
            }
            var count = p - 1;
            for (var i = p; i < r; i++)
            {
                if (points[i].X <= points[r].X)
                {
                    count++;
                }
            }
            var temp = points[++count];
            points[r] = points[count];
            points[count] = temp;
            r = count;
            QuickSort(points, p, count - 1);
            QuickSort(points, count + 1, r);
        }
        #endregion

        #region 'TSP算法'
        /// <summary>
        /// 计算TSP的长度
        /// </summary>
        /// <param name="points"></param>
        /// <param name="m"></param>
        /// <param name="s"></param>
        public static void TSP_Length(Point[] points, ref double[,] m, ref int[,] s)
        {
            var count = points.Length;
            QuickSort(points, 0, count - 1);
            m = new double[count, count];
            s = new int[count, count];
            if (count > 2)
            {
                // case1
                // i = j = count - 1
                // i = count - 2 , j = count - 1
                m[count - 1, count - 1] = 0;
                m[count - 2, count - 1] = GetDistance(points, count - 2, count - 1);
                s[count - 2, count - 1] = count - 1;
                for (var i = count - 3; i >= 0; i--)
                {
                    // case2 
                    // i < j - 1
                    for (var j = count - 1; j > i + 1; j--)
                    {
                        m[i, j] = GetDistance(points, i, i + 1) + m[i + 1, j];
                        s[i, j] = i + 1;
                    }
                    // case3
                    // i = j - 1
                    var min = double.MaxValue;
                    for (var j = i + 2; j <= count - 1; j++)
                    {
                        var temp = m[i + 1, j] + GetDistance(points, i, j);
                        if (temp < min)
                        {
                            min = temp;
                            s[i, i + 1] = j;
                        }
                    }
                    m[i, i + 1] = min;
                }
                s[0, 0] = 1;
                m[0, 0] = m[0, 1] + GetDistance(points, 0, 1);
            }
        }

        /// <summary>
        /// 构造TSP
        /// </summary>
        /// <param name="count"></param>
        /// <param name="m"></param>
        /// <param name="s"></param>
        public static void PrintTSP(int count, double[,] m, int[,] s)
        {
            var left = 0;
            var right = 0;
            var next = 0;
            for (var i = 0; i < count; i++)
            {
                next = s[left, right];
                Console.WriteLine(left.ToString()
                    + "to "
                    + next.ToString());
                if (left == right
                    && left == 0)
                {
                    right = 1;
                }
                else
                {
                    if (left == right - 1)
                    {
                        left = left + 1;
                        right = next;
                    }
                    else if (left < right - 1)
                    {
                        left = left + 1;
                    }
                }
            }
        }
        #endregion



15.5 编辑距离

为了将一个文本串x[1…m] 转换为目标串y[1…n],我们可以使用多种变换操作。我们的目标是,给定x和y ,求将x转换为y的一个变换操作序列。我们使用一个数组z保存中间结果,假定它足够大,可存下中间结果的所有字符。初始时,z是空的;结束时,应有z[j]=y[j],j=1,2,…,n。我们维护两个下标i和j,分别指向x和y中的位置,变换操作允许改变z的内容和这两个下标。初始时,i=j=1。在转换过程中应处理x的所有字符,这意味着在变换操作结束时,应当有i=m+1。

我们可以使用如下6种变换操作:

复制(copy) — 从x复制当前字符到z,即进行赋值z[j]=x[i],并将两个下标i和j都增1。此操作处理了x[i]。

替换(replace) — 将x的当前字符x[i]替换为另一个字符c,然后进行赋值z[j]=c,并将两个下标i ii和j jj都增1。此操作处理了x[i]。

删除(delete) — 删除x中的当前字符x[i],即将i增1,j不变。此操作处理了x[i]。

插入(insert) — 将字符c插入z中,z[j]=c,将j 增1,i不变。此操作未处理x中的字符。

旋转(twiddle,即交换) — 将x xx中的当前字符和下一个字符复制到z中,但交换顺序,z[j]=x[i+1]且z[j+1]=x[i],将i和j都增2。此操作处理了x[i]和x[i+1]。

终止(kill) — 删除x中的剩余字符,令i=m+1。此操作处理了x xx中所有尚未处理的字符。如果执行此操作,则转换过程结束。

下面给出了将源字符串algorithm转换为目标字符串altruistic的一种变换操作序列,下划线指出执行一个变换操作后两个下标i和j的位置:

在这里插入图片描述

每个变换操作都有相应的代价。具体的代价依赖于特定的应用,但我们假定每个操作的代价是一个已知的常量。我们还假定复制和替换的代价小于删除和插入的组合代价,否则复制和替换操作就没有意义了。一个给定的变换操作序列的代价为其中所有变换操作的代价只和。在上例中,将algorithm转换为altruistic的代价为

(3·cost(复制))+ cost(替换) + cost(删除) + (4·cost(替换))+ cost(旋转) + cost(终止)

a.给定两个字符串x[1…m]和y[1…n]以及变换操作的代价,x到y编辑距离是将x转换为y的最小代价的变换操作序列的代价值。设计动态规划算法,求x[1…m]到[1…n]的编辑距离并打印最优变换操作序列。分析算法的时间和空间复杂度。

还是要先理解动态规划设计的通用模式,就如在15.3节中提到的:在确定最优解使用哪些子问题时,我们需要考察多少种选择。

在本题中我们同样要去考察,考察在字符串转换过程中下一步的所有可能选择:

1.当x[i] == y[i]时,此时我们下一步只做复制。

2.当x[i] != y[i]时,此时最优子结构必然是在replace、delete、insert、twiddle、kill中去选择,那么子问题就变成了replace、delete、insert、twiddle、kill搭配更小规模的两个字符串做转换。着重说下旋转,旋转的执行只有一定条件的,它是在当i和j都在可能范围内(i<=x.Length&&j<=y.Length)并且x[i+1] == y[j],x[i] == y[j+1]时才执行。

我们用r[i,j]来存储数组x,y在当前下标为i,j的情况下剩余转换的最小代价值。

        static void Main(string[] args)
        {
            var s_str = "algorithm";
            var d_str = "altruistic";
            var z = new char[d_str.Length];
            var r = new int[s_str.Length + 1, d_str.Length + 1];
            var s = new string[s_str.Length + 1, d_str.Length + 1];
            var value = Execute(s_str.ToArray(), d_str.ToArray(), z, 0, 0, r, s);
            var i = 0;
            var j = 0;
            while (value > 0)
            {
                var str = string.Empty;
                if (s[i, j] == "Copy")
                {
                    i++;
                    j++;
                    str = "Copy";
                }
                else if (s[i, j] == "Replace")
                {
                    i++;
                    j++;
                    str = "Replace";
                }
                else if (s[i, j] == "Delete")
                {
                    i++;
                    str = "Delete";
                }
                else if (s[i, j] == "Insert")
                {
                    j++;
                    str = "Insert";
                }
                else if (s[i, j] == "Twiddle")
                {
                    i = i + 2;
                    j = j + 2;
                    str = "Twiddle";
                }
                else if (s[i, j] == "Kill")
                {
                    i = s_str.Length;
                    str = "Kill";
                }
                Console.WriteLine(str);
                value = r[i, j];
            }
        }

        public static int Execute(char[] x, char[] y, char[] z,
                                        int i, int j, int[,] r, string[,] s)
        {
            if (i == x.Length
                && j == y.Length)
            {
                r[i, j] = 0;
                return 0;
            }
            if (r[i, j] > 0)
            {
                return r[i, j];
            }
            if (i == x.Length)
            {
                var cost = Execute(x, y, z, i, j + 1, r, s)
                           + Insert(ref y, ref z, j);
                s[i, j] = "Insert";
                r[i, j] = cost;
                return cost;
            }
            else if (j == y.Length)
            {
                var cost = Execute(x, y, z, x.Length, j, r, s)
                              + Kill();
                s[i, j] = "Kill";
                r[i, j] = cost;
                return cost;
            }
            var min = int.MaxValue;
            int temp;
            if (x[i] == y[j])
            {
                temp = Execute(x, y, z, i + 1, j + 1, r, s)
                           + Copy(ref x, ref z, i, j);
                min = temp < min ? temp : min;
                s[i, j] = "Copy";
                r[i, j] = temp;
                return temp;
            }
            else
            {
                temp = Execute(x, y, z, i + 1, j + 1, r, s)
                               + Replace(ref y, ref z, j);
                min = temp < min ? temp : min;
                s[i, j] = "Replace";
                r[i, j] = min;
                temp = Execute(x, y, z, i + 1, j, r, s)
                               + Delete();
                min = temp < min ? temp : min;
                s[i, j] = "Delete";
                r[i, j] = min;
                temp = Execute(x, y, z, i, j + 1, r, s)
                               + Insert(ref y, ref z, j);
                min = temp < min ? temp : min;
                s[i, j] = "Insert";
                r[i, j] = min;
                if (i <= x.Length - 2
                    && j <= y.Length - 2)
                {
                    if (x[i] == y[j + 1]
                        && x[i + 1] == y[j])
                    {
                        temp = Execute(x, y, z, i + 2, j + 2, r, s)
                                + Twiddle(ref x, ref z, i, j);
                        min = temp < min ? temp : min;
                        s[i, j] = "Twiddle";
                        r[i, j] = min;
                    }
                }
            }
            return min;
        }

        public static int Copy(ref char[] x, ref char[] z,
                                int i, int j)
        {
            z[j] = x[i];
            return 1;
        }

        public static int Replace(ref char[] y, ref char[] z,
                                    int j)
        {
            z[j] = y[j];
            return 2;
        }

        public static int Delete()
        {
            return 3;
        }

        public static int Insert(ref char[] y, ref char[] z,
                                    int j)
        {
            z[j] = y[j];
            return 3;
        }

        public static int Twiddle(ref char[] x, ref char[] z,
                                    int i, int j)
        {
            z[j] = x[i + 1];
            z[j + 1] = x[i];
            return 2;
        }

        public static int Kill()
        {
            return 5;
        }
    }



15.9 字符串拆分

15.9(字符串拆分)某种字符串处理语言允许程序员将一个字符串拆分为两段。由于此操作需要复制字符串,因此要花费n个时间单位来将一个n个字符的字符串拆为两段。假定一个程序员希望将一个20个字符串在第2个、第8个以及第10个字符串后进行拆分(字符串由左至右,从1开始升序编号)。如果她按由左至右的顺序进行拆分,则第一次拆分花费20个时间单位,第二次拆分花费18个时间单位,第三次拆分花费12个时间单位,共花费50个时间单位。但如果她按由右至左的顺序进行拆分,第一次拆分花费20个时间单位,第二次拆分花费10个时间单位,而第三次拆分花费8个时间单位,共花费38个时间单位。还可以按其他顺序,比如,她可以首先在第8个时间单位进行拆分(时间20),接着在左边一段第2个字符处进行拆分(时间8),最后在右边一段第10个字符处进行拆分(时间12),总时间40.

设计算法,对给定的拆分位置,确定最小代价的拆分顺序。

        static void Main(string[] args)
        {
            int[] L = new int[] { 2, 4, 6, 8, 9, 10 };
            var text = string.Empty;
            for (var i = 0; i < 20; i++)
            {
                text += 1.ToString();
            }
            var r = Execute(text, text.Length, L, 0, L.Length - 1);
        }

        /// <summary>
        /// 最小代价字符串拆分
        /// </summary>
        /// <param name="s"></param>
        /// <param name="length"></param>
        /// <param name="L"></param>
        /// <param name="i"></param>
        /// <param name="j"></param>
        /// <returns></returns>
        public static int Execute(string s, int length,
                                  int[] L, int i, int j)
        {
            if (i > j)
            {
                return 0;
            }
            else if (i == j)
            {
                return length;
            }
            int min = int.MaxValue;
            for (var k = 0; k < j - i + 1; k++)
            {
                var r = Execute(s, L[k + i], L, i, k + i - 1) +
                        Execute(s, 20 - L[k + i], L, k + i + 1, j) +
                        length;
                if (r < min)
                {
                    min = r;
                }
            }
            return min;
        }



版权声明:本文为qq_28871239原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。