相当推荐,暂时我还没有时间看,自己写了一个简单的,呵呵呵呵,留着备用
一、前言
24点游戏是一个常见游戏,出题者给出4个整数,要求答题者在指定时间内给出一个四则运算的表达式,恰好用上这这个整数各一次,计算结果为24,超出时间为输。
二、分析
用计算机来算这个题,搜索速度当然有很大优势,我编程喜欢考虑通用一点,不限制输入数量和结果数,甚至不限制运算符数量。这样组合数就很大,如果输入数比较多,则搜索时间会非常长。
我用两个方法来提高搜索速度:一、是大家都能考虑到的重复搜索问题,比如1,2,3和2,3,1所有的组合情况是相同的,我只搜索使用递增序的数组,则可以降低一个组合数的数量级别;二、使用动态规划中的备忘录方法,比如你计算出2和3所有可能的技术结果,则他们与4结合的时候,要用到,与1结合的时候,也要用到,使用备忘录,可以只计算一次,大大降低运算复杂度。
设A表示输入数组,V表示希望值,O表示运算符集合。
设F(A,O)表示数组A可以选择任意O个运算符,最终可以得到的不同值。
例F({1,2},{+,-})={-1,1,3}。
设A的全非空真子集为T
则F(A,Q)={x|x=F(a,O) o F(b,O),a∈T,o∈O,b=T-a} 这就是动态规划的递归式
三、设计
整体设计:分别设计4个类:游戏、表达式、运算、分数,各司其责,结构清晰,易于扩展。
详细设计:
1、 运算类,非常简单,可以快速跳过
class Operator { public char Symbol; // 运算符 public bool Exchangable;// 是否有交换律 public int Priority; // 优先级,值越大越有效 public Operator(char symbol, bool exchangable, int priority) { Symbol = symbol; Exchangable = exchangable; Priority = priority; } public static Operator Add = new Operator('+', true, 0); public static Operator Sub = new Operator('-', false, 0); public static Operator Mul = new Operator('*', true, 1); public static Operator Div = new Operator('/', false, 1); static List<Operator> operators = null; public static List<Operator> Operators { get { if (operators == null) { operators = new List<Operator>(4); operators.Add(Add); operators.Add(Sub); operators.Add(Mul); operators.Add(Div); } return operators; } } } 2、分数类,设置这个类的目的是可以让除法运算不丢失有效数字 class Fraction : IComparable<Fraction> { public int Numerator; // 分子 public int Denominator = 1; // 分母 public double Value { get { return Numerator * 1.0 / Denominator; } } public Fraction(int numerator) { Numerator = numerator; } /// 进行分数运算 public static Fraction Operate(Fraction f1, Fraction f2, Operator opt) { int n1 = f1.Numerator; int n2 = f2.Numerator; int d1 = f1.Denominator; int d2 = f2.Denominator; Fraction ret = new Fraction(0); switch (opt.Symbol) { case '+': ret.Numerator = n1 * d2 + n2 * d1; ret.Denominator = d1 * d2; ret.Compact(); return ret; case '-': ret.Numerator = n1 * d2 - n2 * d1; ret.Denominator = d1 * d2; ret.Compact(); return ret; case '*': ret.Numerator = n1 * n2; ret.Denominator = d1 * d2; ret.Compact(); return ret; case '/': if (d2 == 0 || n2 == 0) break; ret.Numerator = n1 * d2; ret.Denominator = d1 * n2; ret.Compact(); return ret; } return null; } /// 实现IComparable<Fraction>接口 public int CompareTo(Fraction other) { int v1 = Numerator * other.Denominator; int v2 = Denominator * other.Numerator; return v1 - v2;//Value.CompareTo(other.Value)不如这样精确 } /// 归约分子分母到统一形式,以保证同值的分数比较结果相同 public void Compact() { //分子分母约分 int gcd = GCD(Math.Abs(Numerator), Math.Abs(Denominator)); if (gcd != 0) { Numerator /= gcd; Denominator /= gcd; } if (Denominator < 0) {//调整分母为正 Denominator = -Denominator; Numerator = -Numerator; } } static void Swap(ref int x, ref int y) { x ^= y; y ^= x; x ^= y; } /// 辗转相减法求最大公约数 static int GCD(int x, int y) { if (0 == x) return y; if (0 == y) return x; do { if (x < y) Swap(ref x, ref y); x -= y; } while (x != 0); return y; } } 3、表达式类 class Expression { /// 单数值表达式 public int Number; /// 左子表达式 public Expression LChild; /// 与子表达式运算的运算符 public Operator Operator; /// 右子表达式 public Expression RChild; public Expression(int number) { Number = number;} public Expression(){} // 构造表达式字符串 public override string ToString() { if (Operator == null) {//无子表达式的单独数,直接返回 if (Number < 0) //负数加括号 return "(" + Number.ToString() + ")"; else return Number.ToString(); } string s1 = LChild.ToString(); //如果左子表达式比当前运算符优先级低,需加括号 if (LChild.Operator != null) { int dp = Operator.Priority - LChild.Operator.Priority; if (dp > 0) s1 = "(" + s1 + ")"; } string s2 = RChild.ToString(); //如果右表达式2比当前运算符优先级低 或者 两者同优先级但当前运算符不具备交换律,加括号 if (RChild.Operator != null) { int dp = Operator.Priority - RChild.Operator.Priority; if (dp > 0 || dp == 0 && Operator.Exchangable == false) s2 = "(" + s2 + ")"; } return s1 + Operator.Symbol + s2; } public override bool Equals(object obj) { Expression other = obj as Expression; if (Operator == null || other.Operator == null) { if (Operator != null || other.Operator != null) return false;//操作符存在情况不一致 return Number == other.Number; } if (Operator.Symbol != other.Operator.Symbol) return false; if (Operator.Exchangable == false) {//不可交换运算,左右子表达式必须严格相等 if (LChild.Equals(other.LChild) ==false || RChild.Equals(other.RChild) ==false ) return false ; } else {//可交换运算,左右交叉也认为是相同表达式 if ((LChild.Equals(other.RChild) == false || RChild.Equals(other.LChild) == false) && (LChild.Equals(other.LChild) == false || RChild.Equals(other.RChild) == false)) return false ; } return true ; } } 4、游戏类,核心算法在这里 class EquationGame { /// 备忘录,记录由哪组数,可以达到哪些值,对应的(1个)表达式是怎样 public Dictionary<string, SortedList<Fraction, Expression>> memo = new Dictionary<string, SortedList<Fraction, Expression>>(); ///记录哪个子数组和另外一个子数组是否交叉计算过 public Hashtable hashTable = new Hashtable(); public int[] array;//数组 public int n;//数组大小 public EquationGame(int[] array) { Array.Sort(array); this.array = array; this.n = array.Length; } public Expression GetOneExpression(int r) { //计算所有可能的最终结果,存入备忘录 BuildAllPossbile(array); //查找最终结果为要求数的表达式 string strAll = GetKeyString(array); Fraction fra = new Fraction(r); if (memo[strAll].ContainsKey(fra)) return memo[strAll][fra]; return null; } /// 构造由arr里面的每个数用一次运算的不同计算结果 /// 并保留对应表达式,核心算法在这里 void BuildAllPossbile(int[] arr) { string strKey = GetKeyString(arr); if (memo.ContainsKey(strKey) == true) return;//已存在,不用再计算 SortedList<Fraction, Expression> list = new SortedList<Fraction, Expression>(); if (arr.Length == 1) {//单个数直接添加 Expression exp = new Expression(arr[0]); list.Add(new Fraction(arr[0]), exp); } //子串长度,根据组合公式C(n,r)=C(n,n-r),只搜索到一半就完成 for (int len = 1; len <= arr.Length / 2; len++) { //将arr分成两个数组,一个长度为len,一个为arr.Legnth-len,求所有可能的组合 int[] a1 = new int[len]; int[] a2 = new int[arr.Length - len]; int[] select = new int[arr.Length];//当前组合选入a1的数在arr中的下标 for (int i = 0; i < len; i++) select[i] = i; while (true) { //将选择映射到两个数组 int c1 = 0, c2 = 0, si = 0; for (int i = 0; i < arr.Length; i++) { if (select[si] == i) { a1[c1++] = arr[i]; //前半数组 si++; } else a2[c2++] = arr[i]; //后半数组 } string hKey = GetKeyString(a1) + ' ' + GetKeyString(a2); if (hashTable.Contains(hKey) == false) {//如果a1和a2没有计算过,则进行计算 hashTable.Add(hKey, null); BuildAllPossbile(a1); BuildAllPossbile(a2); CalculateAll(a1, a2, list); } //调整到下一个可能 比如5选2有 //找下一个可调整位置 int t = len - 1; while (t >= 0 && select[t] == arr.Length - len + t) t--; if (t == -1) break; select[t]++; for (int j = t + 1; j < len - 1; j++) select[j] = select[j - 1] + 1; } } memo[strKey] = list; } /// 计算a1和a2所有可能的运算结果,保存到list void CalculateAll(int[] a1, int[] a2, SortedList<Fraction, Expression> list) { SortedList<Fraction, Expression> list1 = memo[GetKeyString(a1)]; SortedList<Fraction, Expression> list2 = memo[GetKeyString(a2)]; Fraction fra; foreach (Fraction fra1 in list1.Keys) foreach (Fraction fra2 in list2.Keys) foreach (Operator opt in Operator.Operators) for (int k = 0; k < 2; k++) { if (k == 1 && opt.Exchangable == true)//满足交换律只算一次 break; if (k == 0) fra = Fraction.Operate(fra1, fra2, opt); else fra = Fraction.Operate(fra2, fra1, opt); if (fra == null) break; //如果当前计算值不存在,才加入list,如果要求所有解,则是不相同就能加入list if (list.ContainsKey(fra) == false) { //构造新的表达式加入进来 Expression exp = new Expression(); exp.Operator = opt; if (k == 1) { exp.LChild = list2[fra2]; exp.RChild = list1[fra1]; } else { exp.LChild = list1[fra1]; exp.RChild = list2[fra2]; } list[fra] = exp; } } } /// 根据数组产生备忘录的索引字符串 string GetKeyString(int[] arr) { StringBuilder sb = new StringBuilder(); int index = 0; int last = arr.Length - 1; foreach (int num in arr) { sb.Append(num); if (index < last) sb.Append(','); index++; } return sb.ToString(); } public List<Expression> GetAllExpresses(int r) { //计算所有可能的最终结果,存入备忘录 BuildAllPossbile(array); return GetAllExpresses(array, new Fraction(r)); } /// 求arr拆分之后,所有可能达到r的表达式的集合 List<Expression> GetAllExpresses(int[] arr, Fraction r) { List<Expression> list = new List<Expression>(); string strKey = GetKeyString(arr); if (memo.ContainsKey(strKey) == false) return list;//查表不可达到,直接放弃搜索 if (arr.Length == 1) { if (new Fraction(arr[0]).CompareTo(r) == 0) list.Add(new Expression(arr[0])); return list; } //子串长度,根据组合公式C(n,r)=C(n,n-r),只搜索到一半就完成 for (int len = 1; len <= arr.Length / 2; len++) { //将arr分成两个数组,一个长度为len,一个为arr.Legnth-len,求所有可能的组合 int[] a1 = new int[len]; int[] a2 = new int[arr.Length - len]; int[] select = new int[arr.Length];//当前组合选入a1的数在arr中的下标 for (int i = 0; i < len; i++) select[i] = i; while (true) { //将选择映射到两个数组 int c1 = 0, c2 = 0, si = 0; for (int i = 0; i < arr.Length; i++) { if (select[si] == i) { a1[c1++] = arr[i]; //前半数组 si++; } else a2[c2++] = arr[i]; //后半数组 } CalculateEqualFra(a1, a2, list, r); //调整到下一个可能 比如5选2有 //找下一个可调整位置 int t = len - 1; while (t >= 0 && select[t] == arr.Length - len + t) t--; if (t == -1) break; select[t]++; for (int j = t + 1; j < len - 1; j++) select[j] = select[j - 1] + 1; } } return list; } /// 计算a1和a2所有可能的运算结果,保存到list void CalculateEqualFra(int[] a1, int[] a2, List<Expression> list, Fraction r) { SortedList<Fraction, Expression> list1 = memo[GetKeyString(a1)]; SortedList<Fraction, Expression> list2 = memo[GetKeyString(a2)]; Fraction fra; foreach (Fraction fra1 in list1.Keys) foreach (Fraction fra2 in list2.Keys) foreach (Operator opt in Operator.Operators) for (int k = 0; k < 2; k++) { if (k == 1 && opt.Exchangable == true)//满足交换律只算一次 break; if (k == 0) fra = Fraction.Operate(fra1, fra2, opt); else fra = Fraction.Operate(fra2, fra1, opt); if (fra == null) break; //如果计算值等于r,则加入list if (fra.CompareTo(r) == 0) { //构造新的表达式加入进来 Expression exp = new Expression(); //递归的求子表达式有哪些情况 List<Expression> l1 = GetAllExpresses(a1, fra1); List<Expression> l2 = GetAllExpresses(a2, fra2); exp.Operator = opt; //然后组合在一起 foreach (Expression exp1 in l1) foreach (Expression exp2 in l2) { if (k == 1) { exp.LChild = exp2; exp.RChild = exp1; } else { exp.LChild = exp1; exp.RChild = exp2; } if (list.Contains(exp) == false) list.Add(exp); } } } } }
四:后记
现在编个算法题也搞这么多类,面向对象思想已经深入大脑了,因为其概念清晰,不然混在一堆,乱糟糟的。原来以为写个穷举搜索也就10分钟,但又不甘心写穷举,能优化的当然要优化,加上很久不编算法程序,花了大半个白天,逐步得出这套思路。