yinger

原文链接:http://eol.gzu.edu.cn/eolcourse/common/blog/userBlogArticleView.jsp?bkt=%2Feolcourse%2Fcommon%2Fblog%2FuserBlogColumnArticle.jsp%3FblogId%3D6444%26columnId%3D2664&articleId=6164

相当推荐,暂时我还没有时间看,自己写了一个简单的,呵呵呵呵,留着备用

一、前言

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分钟,但又不甘心写穷举,能优化的当然要优化,加上很久不编算法程序,花了大半个白天,逐步得出这套思路。

分类:

技术点:

相关文章:

猜你喜欢
  • 2021-07-31
相关资源
相似解决方案