求助 简单的递归算法

杨戴沐 2012-04-18 07:33:55
一组数的规则如下:1、1、2、3、5、8、13、21、34.....,求第30位数是多少?用递归方式实现

另外还有两个面试题,求解答。
1、A说B说谎,B说C说谎,C说A、B说谎,请问到底谁说谎?
2、有一个三升的水杯和一个五升的水杯,如何倒四升的水?

智商拙计 真心求帮助 万分感谢!
...全文
536 37 打赏 收藏 转发到动态 举报
写回复
用AI写文章
37 条回复
切换为时间正序
请发表友善的回复…
发表回复
杨戴沐 2012-05-11
  • 打赏
  • 举报
回复
楼上的都是高手 都是大神 膜拜ing...
  • 打赏
  • 举报
回复
顺便骂一下csdn,ie 9.0 下回复区的工具栏(如“插入源代码”,“插入超链接”等图标)无法显示,害得我只能用opera来回复。shit!
yangchun1213 2012-04-22
  • 打赏
  • 举报
回复
第一个数列的东东就不用多说了,其他两个用程序来实现还真有点难度
cnwin 2012-04-22
  • 打赏
  • 举报
回复
看过好多Sp1234所回的帖子,对sp1234表示由衷的钦佩。
cnwin 2012-04-22
  • 打赏
  • 举报
回复
[Quote=引用 32 楼 的回复:]
顺便骂一下csdn,ie 9.0 下回复区的工具栏(如“插入源代码”,“插入超链接”等图标)无法显示,害得我只能用opera来回复。shit!
[/Quote]
呵呵,这个问题我曾反馈过。不过可以点击ie9地址栏后面的兼容性视图可以解决工具栏问题。
yp19910928 2012-04-22
  • 打赏
  • 举报
回复
第一题是斐波那契数列
int Fibonacci(int n)
{
if( n == 1 || n == 2) // 递归结束的条件,求前两项
return 1;
else
return Fibonacci(n-1)+Fibonacci(n-2); // 如果是求其它项,先要求出它前面两项,然后做和。
}
第二题最笨的方法是三个for循环遍历
第三题是
方法一
1.用3升的容器接满水,倒入5升容器中。
2.再用3升的容器接满,倒入5升容器中。此时3升容器中还剩下1升水。
3.将5升容器中的水倒掉,将3升容器中剩下的1升水倒入5升容器。
4.再将3升容器接满水倒入5升容器中,此时5升容器中就是4升水。

方法二
1.用5升的容器接满水,倒入3升容器中。此时5升容器中有2升水。
2.将3升容器中的水倒掉,在将5升容器中剩下的水倒入3升容器中。此时3升容器中有2升水。
3.将5升容器接满水,把水再倒入3升容器中至满。此时5升容器中剩4升水。
  • 打赏
  • 举报
回复
[Quote=引用 29 楼 的回复:]

具体地说,不是“在程序里硬性规定如果步骤大于N,就认为无解”,而是应该在发现目标状态重复时就停止深度搜索而是去回溯其它动作。

同时我其实在#21楼要表达的才是我最想分享的算法。那里可以包括你能想到的其它几种“解法”,而且用搜索树可以遍历所有解法,不是只能知道一个解。
[/Quote]
目标状态重复就停止深度搜索以及用搜索树遍历,这些我也想到了。问题是对具体实现有点疑问,请指教:
1.这样意味着必须保存所有搜索状态并比较,这样效率(性能)会不会有问题?
2.搜索树的层次如何定?这个层次等于是这里所说的操作步数吧。

所以基于这个考虑,可能还是硬性规定最多搜索几步简单些。
  • 打赏
  • 举报
回复
另外的方法是用递归,实际也有两种不同的写法,这里先改写一下sp1234的写法:

StringBuilder sb = new StringBuilder();
Action<int, int, bool, int> pullWater = null;
pullWater = (a, b, isFromSmallToBig, count) =>
{
if (b == 4 || count > 20) //最多试20步
return;
if (isFromSmallToBig)
{
if (b == 5) //如果5升的杯子满了
{
sb.AppendLine("empty big cup;");
pullWater(a, 0, true, count++);
}
else if (a == 0) //3升的杯子是空的
{
sb.AppendLine("fill up small cup;");
pullWater(3, b, true, count++);
}
else if (a + b <= 5) //倒水不会溢出
{
sb.AppendLine("pull " + a.ToString() + " l water to big cup and "
+ "big cup now has " + (a + b).ToString() + " l water;");
pullWater(0, a + b, true, count++);
}
else
{
sb.AppendLine("small cup has " + a.ToString() +
" l water and pull " + (5 - b).ToString() + " l water to big cup.");//{0}升水其中{1}升倒入5升杯子。", a, 5 - b);
pullWater(a + b - 5, 5, true, count++);
}
}
else
{
if (a == 3) //如果3升的杯子满了
{
sb.AppendLine("empty small cup;");
pullWater(0, b, false, count++);
}
else if (b == 0) //5升的杯子是空的
{
sb.AppendLine("fill up big cup;");
pullWater(a, 5, false, count++);
}
else if (a + b <= 3) //倒水不会溢出
{
sb.AppendLine("pull " + b.ToString() + " l water to small cup and "
+ "small cup now has " + (a + b).ToString() + " l water;");
pullWater(a + b, 0, false, count++);
}
else
{
sb.AppendLine("big cup has " + b.ToString() +
" l water and pull " + (3 - a).ToString() + " l water to small cup.");//{0}升水其中{1}升倒入5升杯子。", a, 5 - b);
pullWater(3, a + b - 3, false, count++);
}
}
};
pullWater(0, 0, true, 0);
sb.AppendLine("------------------");
pullWater(0, 0, false, 0);

  • 打赏
  • 举报
回复
具体地说,不是“在程序里硬性规定如果步骤大于N,就认为无解”,而是应该在发现目标状态重复时就停止深度搜索而是去回溯其它动作。

同时我其实在#21楼要表达的才是我最想分享的算法。那里可以包括你能想到的其它几种“解法”,而且用搜索树可以遍历所有解法,不是只能知道一个解。
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 的回复:]
倒水那个问题,sp1234有几个问题没考虑到:
1.把b == 4作为递归结束条件,在有解的情况下是可以的,但如果无解,将堆栈溢出。好像难以确定这个递归的结束条件,所以我在程序里硬性规定如果步骤大于N,就认为无解,不再考虑
[/Quote]

实际上我在#20和#21楼也做了简单说明。当时写那个程序,就是假设我们都知道这类题目可以这样机械地简单求解为前提。而只要两个杯子的容积(一个3升、一个5升)互为素数,就一定可以求解。程序中把3、5这两个数都写死了,也就无需判断是否无解了。

我在#21楼说明了一个通用的问题求解基本的搜索树结构和算法(在两种情况下会自动停止搜索)。实际上已经在一个通用解题方法上说明了这个判断。
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 的回复:]
但是程序有点小问题,输出结果是:
第二题(说真话):A = True, B = False, C = True。刚好和正确答案反了。
不过改正也很容易,都取反即可,即:
Console.WriteLine("第二题(说真话):A={0}……
[/Quote]

在 #18 楼我做了说明。
  • 打赏
  • 举报
回复
上面这句sb.Remove(98, 1);粘贴错了,应是sb.Remove(15,1);
  • 打赏
  • 举报
回复
这个改进的方法利用了2进制表示,程序简洁许多,而且可以定义最多搜索几步,找出最快的2种方法

public class Cups
{
public int CupA { get; set; }
public int CupB { get; set; }
}

Cups cups = new Cups();
bool result = false;
SortedList<string, string> steps = new SortedList<string, string>();//存储操作步骤
List<string> quickest = new List<string>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++)
{//只计算最多15步,所以这里初始成16步,相当于“无限大”
sb.Append("1");
}
sb.Append("0");
quickest.Add(sb.ToString());
sb.Remove(98, 1);
sb.Append("1");
quickest.Add(sb.ToString());
sb.Length = 0;
for (int i = 0; i < 32768; i++)
{//32768 = 2^15,也就是说最多计算15步
//转化成2进制,每1位代表一个操作步骤,0表示从小杯往大杯倒水
//1表示从大杯往小杯倒水。这里i取0到2^15,穷尽15步内的所有可能操作
string stepString = Convert.ToString(i, 2);
//注意对于不足15位的数,前面必须补0,不然类似0010,0000的操作步骤就被忽略了
for (int j = 0; j < 15 - stepString.Length; j++)
{
sb.Append("0");
}
sb.Append(stepString);
stepString = sb.ToString();
sb.Length = 0;
bool isContinue = false;
foreach (string key in steps.Keys)
{//检查是否与已有结果重复,但这其实是不完善的,原因上面已经解释了
if (stepString.StartsWith(key))
{
isContinue = true;
break;
}
}
if (isContinue)
{
continue;
}
cups.CupA = 3;//复位到初始状态,小杯3升水,大杯5升
cups.CupB = 5;
StringBuilder tmpSteps = new StringBuilder();//复位临时的操作步骤
for (int j = 0; j < stepString.Length; j++)
{//
string step = "";
result = pullWater3(stepString[j], cups, out step);//倒水操作
tmpSteps.AppendLine(step);
if (result)
{//若得到一个解
//获得当前操作步骤的2进制字符串表示
string substeps = stepString.Substring(0, j);
if (checkQuickest3(quickest, substeps))
{//如果属于最快的方法之一,加入到最终的输出结果集
steps.Add(substeps, tmpSteps.ToString());
}
break;//没必要走完15步,继续找下一种方法
}
}
}
if (steps.Count > 0)
{//如有解,输出结果
outputSteps3(steps, quickest);
}
}

private void outputSteps3(SortedList<string, string> steps, List<string> quickest)
{//输出结果
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 2; i++)
{//找到最快的2种方法
if (steps.ContainsKey(quickest[i]))
{
sb.Append(steps[quickest[i]]);
sb.AppendLine("----------------------");
}
}
txtResult.Text = sb.ToString();
}

private bool checkQuickest3(List<string> quickest, string candidateSteps)
{//检查是否是最快的方法之一
bool isQuickest = false;
for (int i = 0; i < 2; i++)
{//只找最快的前2种,排除重复
if (candidateSteps.Length < quickest[i].Length
|| (candidateSteps.Length == quickest[i].Length
&& candidateSteps != quickest[i])
)
{
quickest.Insert(i, candidateSteps);
isQuickest = true;
break;
}
}
return isQuickest;
}

private bool pullWater3(char type, Cups cups, out string step)
{//倒水操作,类似sp1234,不过考虑了从小杯倒大杯和从大杯倒小杯两种情况
step = "";
StringBuilder sb = new StringBuilder();
int tofill = 0;
if (cups.CupB == 4)
{
return true;
}

if (type == '0')
{//从小杯到大杯
if (cups.CupB == 5)
{//大杯满
sb.Append(" 倒空大杯;");
cups.CupB = 0;
}
tofill = 5 - cups.CupB;
if (cups.CupA == 0)
{//小杯空
sb.Append(" 加满小杯;");
cups.CupA = 3;
}
if (cups.CupA <= 5 - cups.CupB)
{
tofill = cups.CupA;
}
sb.Append(" 从小杯倒 " + tofill.ToString() + " 升水倒大杯;");
cups.CupA -= tofill;
cups.CupB += tofill;
}
else
{//大杯到小杯
if (cups.CupA == 3)
{
sb.Append(" 倒空小杯;");
cups.CupA = 0;
}
tofill = 3 - cups.CupA;
if (cups.CupB == 0)
{
sb.Append(" 加满大杯;");
cups.CupB = 5;
}
if (cups.CupB <= 3 - cups.CupA)
{
tofill = cups.CupB;
}
sb.Append(" 从大杯倒 " + tofill.ToString() + " 升水到小杯;");
cups.CupA += tofill;
cups.CupB -= tofill;
}
step = sb.ToString();
if (cups.CupB == 4)
{
return true;
}
else
{
return false;
}
}

  • 打赏
  • 举报
回复
赫赫,看来抛砖还真引来了玉。

说谎那个问题,sp1234的方法给我很大启发,函数式编程的思想很好,我对函数式编程理解太浅,所以想不到。谢谢指教!。
但是程序有点小问题,输出结果是:
第二题(说真话):A = True, B = False, C = True。刚好和正确答案反了。
不过改正也很容易,都取反即可,即:
Console.WriteLine("第二题(说真话):A={0} B={1} C={2}", !query.A, !query.B, !query.C);

倒水那个问题,sp1234有几个问题没考虑到:
1.把b == 4作为递归结束条件,在有解的情况下是可以的,但如果无解,将堆栈溢出。好像难以确定这个递归的结束条件,所以我在程序里硬性规定如果步骤大于N,就认为无解,不再考虑,当然这个N设成比较“合理”的一个数。因为这个算法复杂性好像是2^n,也就是说是无效算法,所以把N设得小一些,比如20。
2.这个解法,只要找到一个结果就结束。我的程序还考虑到更多的可能。比如该题就有两种解法,1楼已经提到了。当然,如何区分两种解法是否不同有点tricky的地方,比如我上面说的,先从大杯往小杯倒2升水,小杯再往大杯倒2升水,愚蠢的计算机认为是一种新的倒法,实际这是两步无效操作。如果定义和排除这种无效操作我还没想好。
3.只从一个杯子往另一个杯子倒,固然可能简化了思路,但也可能忽略了可能的解法。

当然,我上面的程序虽然经过修改,但是显然是很粗糙的,如果sp1234的简洁,这点必须承认。不过我思考了一下,又想出两种简化的方法,如下:
  • 打赏
  • 举报
回复
当然如果用c#来写一个通用的智能程序,那么可能你需要一个描述某一个“活物”的抽象父类(例如这里可以用“杯子”这个具体class来实现),其中有一个Action集合(这个“杯子”里边有四种动作),描述每一个动作的条件表达式、结果表达式。然后你创造一个通用的“问题空间”对象,把所有的“活物”插入问题空间,然后由这个空间去搜索出含有解答的树或者连接就行了。

这个程序(抽象核心数据结构和解题流程部分)写起来很容易,有个几十行代码足以。
  • 打赏
  • 举报
回复


呵呵,对于第三题,这类问题求解就是:一直用一个杯子盛水向另外一个杯子里倒就行了,总是能解决问题(除非其它倒法也无解)。所以这里是个简化解法。


一方面,这个程序写成了尾递归。可以直接改为迭代,例如:
Action<int, int> 倒水 = null;
倒水 = (a, b) =>
{
begin:
if (b == 4)
return;
else if (b == 5) //如果5升的杯子满了
{
Console.Write("到空5升杯子。");
b = 0;
goto begin;
}
else if (a == 0) //3升的杯子是空的
{
Console.Write("倒满3升杯子。");
a = 3;
goto begin;
}
else if (a + b <= 5) //倒水不会溢出
{
Console.Write("{0}升水倒入杯子成{1}升。", a, a + b);
b = a + b;
a = 0;
goto begin;
}
else
{
Console.Write("{0}升水其中{1}升倒入5升杯子。", a, 5 - b);
a = a + b - 5;
b = 5;
goto begin;
}
};



另一方面,如果是一个通用的只能求解问题,那么我们应该使用问题树来描述状态。假设以自底向上搜索方法(树顶就是问题的底——初始状态),我们将可能的操作分为8种:
1. 倒空5升的杯子;(条件是杯子中有水)
2. 倒满5升的杯子;(条件是杯子中的水不足5升)
3. 倒空3升的杯子;(条件是杯子中有水)
4. 倒满3升的杯子;(条件是杯子中的水不足3升)
5. 将5升杯子里的水全部倒到3升杯子里; (条件是杯中有水,并且倒后不会溢出)
6. 将5升杯子里的部分水倒到3升杯子里直到满; (条件是杯中有水,并且全倒入会溢出)
7. 将3升杯子里的水全部倒到5升杯子里; (条件是杯中有水,并且倒后不会溢出)
8. 将3升杯子里的部分水倒到5升杯子里直到满; (条件是杯中有水,并且全倒入会溢出)
那么从每一个状态开始,只要条件满足我们就可以使用8个操作中的方法得到下一个状态分枝,从而构造子目标问题树。

然后,如果得到目标状态,那么就可以(在这个分枝上)停止递归构造子树了,而且可以把目标状态到树顶的路径作为解答。

同时如果发现了与其它状态重复的状态,那么这个分枝也应该停止递归构造子树了。重复的子目标可以直接消除。

这就是一般的问题求解思路。各种解法不过是在“自顶向下还是自底向上、状态归结还是自动匹配、并行还是顺序处理、使用树结构还是连接图结构”等问题上的分歧。而此问题的最基本数据结构和最基本的算法思路并没有多大的变化。
  • 打赏
  • 举报
回复
当然对于自然语言,我们的理解可能不同。你可能把

where a != b && b != c && c != (a && b)

写成

where a != b && b != c && c != (a || b)

这也是可能的。这就是不同人、或者同一人在不同情景下可能产生的歧义。
  • 打赏
  • 举报
回复
[Quote=引用 7 楼 的回复:]

既然prolog编不来,尝试用c#编,开始觉得不难,可是昨天搞了一下午,仍然有bug,今天又搞了一会,总算初步能工作了,代码又臭又长,只不过花了不少时间写的,敝帚自珍吧。如果有高手指导我改进或者给出更好的思路,不胜感激!

C# code

private void button1_Click(object sender, EventArgs e) //做了个win form程序
{
……
[/Quote]

我写了个简单c#的。

使用prolog显然应该比c#代码更简单,看来你的prolog没有学好。
  • 打赏
  • 举报
回复
[Quote=引用楼主 的回复:]
一组数的规则如下:1、1、2、3、5、8、13、21、34.....,求第30位数是多少?用递归方式实现

另外还有两个面试题,求解答。
1、A说B说谎,B说C说谎,C说A、B说谎,请问到底谁说谎?
2、有一个三升的水杯和一个五升的水杯,如何倒四升的水?

智商拙计 真心求帮助 万分感谢!
[/Quote]

题目本身很简单。这主要是看你受的计算机编程(和逻辑)教育水平,如果没有相应的正规教育,那么可能就算是能说出来、也不会编程。我不知道现在的教软件的教师怎样,如果你找个早先真正搞软件教育的老师,这类题目可以很容易得到答案。

随便写一种demo,从代码你就会发现其实很简单。所以,主要是不是这个程序有多难(只要你对c#3.0以后的语法不陌生的话),而是很多人竟然从来没有这样想过这类编程问题。

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Func<int, long> fib = null;
fib = n =>
{
if (n <= 2)
return 1;
else
return fib(n - 1) + fib(n - 2);
};
Console.WriteLine("第一题:{0}", fib(30));
(from a in 真话()
from b in 真话()
from c in 真话()
where a != b && b != c && c != (a && b)
select new { A = a, B = b, C = c })
.ToList()
.ForEach(query =>
{
Console.WriteLine("第二题(说真话):A={0} B={1} C={2}", query.A, query.B, query.C);
});
Action<int, int> 倒水 = null;
倒水 = (a, b) =>
{
if (b == 4)
return;
else if (b == 5) //如果5升的杯子满了
{
Console.Write("到空5升杯子。");
倒水(a, 0);
}
else if (a == 0) //3升的杯子是空的
{
Console.Write("倒满3升杯子。");
倒水(3, b);
}
else if (a + b <= 5) //倒水不会溢出
{
Console.Write("{0}升水倒入杯子成{1}升。", a, a + b);
倒水(0, a + b);
}
else
{
Console.Write("{0}升水其中{1}升倒入5升杯子。", a, 5 - b);
倒水(a + b - 5, 5);
}
};
倒水(0, 0);
Console.ReadKey();
}

static IEnumerable<bool> 真话()
{
yield return true;
yield return false;
}
}

}


我出上机考试的题,不会出这种跟我们的工作差别太大的问题。
hzzasdf2 2012-04-20
  • 打赏
  • 举报
回复
因同一用户不能连续回贴3次,用马甲回。



private bool pullWater(bool isFromSmallToBig, Cups cups, List<string> steps)
{//模拟倒水操作
int tofill = 0;
if (cups.CupB == 4)
{//若操作前大杯已有4升水,直接返回
return true;
}

if (isFromSmallToBig)
{//小杯倒大杯
if (cups.CupB == 5)
{//若大杯满,先倒空
cups.CupB = 0;
}
tofill = 5 - cups.CupB;//可向大杯里倒的水量
if (cups.CupA == 0)
{//若小杯空,先装满后再倒
cups.CupA = 3;
}
if (cups.CupA <= 5 - cups.CupB)
{//若小杯水倒不满大杯
tofill = cups.CupA;
}
steps.Add("true:" + tofill.ToString());//记录操作步骤
cups.CupA -= tofill;
cups.CupB += tofill;
}
else
{//大杯倒小杯
if (cups.CupA == 3)
{//若小杯满,先倒空
cups.CupA = 0;
}
tofill = 3 - cups.CupA;//可往小杯倒的水量
if (cups.CupB == 0)
{//若大杯空,先倒满
cups.CupB = 5;
}
if (cups.CupB <= 3 - cups.CupA)
{
tofill = cups.CupB;
}
steps.Add("false:" + tofill.ToString());
cups.CupA += tofill;
cups.CupB -= tofill;
}
if (cups.CupB == 4)
{//若大杯有4升水
return true;
}
else
{
return false;
}
}
加载更多回复(13)

62,046

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术交流专区
javascript云原生 企业社区
社区管理员
  • ASP.NET
  • .Net开发者社区
  • R小R
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

.NET 社区是一个围绕开源 .NET 的开放、热情、创新、包容的技术社区。社区致力于为广大 .NET 爱好者提供一个良好的知识共享、协同互助的 .NET 技术交流环境。我们尊重不同意见,支持健康理性的辩论和互动,反对歧视和攻击。

希望和大家一起共同营造一个活跃、友好的社区氛围。

试试用AI创作助手写篇文章吧