新手编程:一种24点游戏的通用计算方法(Python+C语言实现)

1、新手编程

新手学习编程总会遇到一个问题,从什么例子开始练手呢?如果是“Hello World"程度的程序,做完了之后可能会觉得,原来编程只是这样的程度吗?但是如果实现一个比较难的任务,比方说“徒手解码MP3音频文件”,那又可能无从下手,容易劝退新人。不过我最近想到一个合适的例子,那就是计算24点。

计算24点看起来简单,规则大家都能理解,但是怎么通过抽象化的函数求解呢?结果又怎么打印出来呢?求解目标可以不是24而是别的数值吗?输入数字可以是任意数目个数吗?穷举求解的时候能不能排除等效算式呢?打印显示的时候能不能去除多余的括号呢?算法优化呢?

总之这道题目可以简单也可以困难,可甜可咸,新手可以根据自己的兴趣和对编程的理解程度,自己给自己出合适难度的题目。

2、设计方法

我给自己出的题目是,对于任意数量的数字,求解包含加减乘除运算的等式,能够满足得到指定数目结果

首先将计算24点的过程,抽象为任取2个数进行“操作”,操作包含加减乘除,以及因前后顺序调换而不同的总共6种可能:

exps = ['({0} + {1})', '({0} - {1})', '({1} - {0})',
        '({0} * {1})', '({0} / {1})', '({1} / {0})']

然后穷举“抽数”顺序,采用抽取不放回策略:

for nums2 in itertools.permutations(nums)

穷举“操作”可能性,抽取放回,有N个数就要有N-1次“操作”:

for exps2 in itertools.product(exps, repeat=len(nums)-1)

按照顺序逐一取出2个数,对6种“操作”的可能性进行算式的拼接,完成拼接的算式看成一个新的“数字”,和接下来的一个数字再次进行“拼接”。并且每次拼接的算式最外层已经包含括号,所以不存在运算优先级的问题:

n1 = nums2[0]
for n2, exp2 in zip(nums2[1:], exps2):
    n1 = exp2.format(n1, n2)

对拼接的算式尝试“求解”,其中排除除数存在等于0的情况,并验证是否和目标结果相等:

由于计算机计算存在浮点数误差的问题,所以当和目标结果的差值的小于精度误差时,就可以认为计算结果是相等的(反例:24 == 8 / (3 - (8 / 3))),并返回结果(如无结果返回None):

try:
    if abs(result - eval(n1)) < 1e-6:
        return '{} == {}'.format(result, n1)
except ZeroDivisionError:
    pass

3、Python版本

3.1 程序代码

import itertools

def calc(nums, result):
    exps = ['({0} + {1})', '({0} - {1})', '({1} - {0})',
            '({0} * {1})', '({0} / {1})', '({1} / {0})']
    for exps2 in itertools.product(exps, repeat=len(nums)-1):
        for n1, *nums2 in itertools.permutations(nums):
            for n2, exp2 in zip(nums2, exps2):
                n1 = exp2.format(n1, n2)
            try:
                if abs(result - eval(n1)) < 1e-6:
                    return '{} == {}'.format(result, n1)
            except ZeroDivisionError:
                pass

3.2 统计用时

import time

time.clock()
sum = cnt = 0
for nums in itertools.combinations_with_replacement(range(10), 4):
    sum += 1
    if calc(nums, 24):
        cnt += 1
print(time.clock())
print(f'{cnt}/{sum}={cnt/sum:.2%}')

3.3 测试结果

4个数字取0-9范围:

共有715种组合,其中446种有解,占比62.38%,运行23秒完成计算。

4个数字取1-9范围:

共有495种组合,其中384种有解,占比77.58%。

3.4 最新更新

我忽然发现算法有Bug!!!最新代码,不解释了,我好累:

import time
import itertools

def calc(nums, target):
    exps = ['({0} + {1})', '({0} - {1})', '({1} - {0})',
            '({0} * {1})', '({0} / {1})', '({1} / {0})']
    if len(nums) == 1:
        try:
            if abs(target - eval(nums[0])) < 1e-6:
                yield nums[0]
        except ZeroDivisionError:
            pass
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            others = nums[:i] + nums[i+1:j] + nums[j+1:]
            for exp in exps:
                yield from calc([exp.format(nums[i], nums[j]), *others], target)


time.clock()
sum = cnt = 0
for nums in itertools.combinations_with_replacement(range(1, 11), 4):
    sum += 1
    for exp in calc(nums, 24):
        cnt += 1
        break
print(time.clock())
print(f'{cnt}/{sum}={cnt/sum:.2%}')

计算结果:

共有715种组合,其中466种有解,占比65.17%,用时19秒完成。

4、面向对象的求解方法

前面的Python解法用了eval,所以运行速度较慢。这次又用一种面向对象的方式实现:

import time
import itertools

class Num:
    def __init__(self, v, a=None, b=None, op=None):
        self.v = v
        self.a = a
        self.b = b
        self.op = op

    def __eq__(self, x):
        return abs(self.v - x) < 1e-6

    def __add__(self, b):
        return Num(self.v + b.v, self, b, '+')

    def __sub__(self, b):
        return Num(self.v - b.v, self, b, '-')

    def __mul__(self, b):
        return Num(self.v * b.v, self, b, '*')

    def __truediv__(self, b):
        if b.v:
            return Num(self.v / b.v, self, b, '/')
        else:
            return Num(float('nan'), self, b, '/')

    def __str__(self):
        return f'{self.v}'

    def __repr__(self):
        if self.a is None:
            return f'{self.v}'
        else:
            return f'({self.a!r} {self.op} {self.b!r})'

def calc(nums):
    nums = [n if isinstance(n, Num) else Num(n) for n in nums]
    funcs = [Num.__add__, Num.__mul__, Num.__sub__, Num.__truediv__]
    if len(nums) == 1:
        yield nums[0]
    for i in range(len(nums)):
        for j in range(len(nums)):
            others = [n for k, n in enumerate(nums) if k not in (i, j)]
            for k, func in enumerate(funcs):
                if i < j or i > j and k > 1:
                    yield from calc([func(nums[i], nums[j]), *others])

time.clock()
sum = cnt = 0
for nums in itertools.combinations_with_replacement(range(10), 4):
    sum += 1
    for exp in calc(nums):
        if exp == 24:
            repr(exp)
            cnt += 1
            break
print(time.clock())
print(f'{cnt}/{sum}={cnt/sum:.2%}')

运行时间7.8秒。

5、C语言版本

5.1 计算结果

前面使用Python编写的软件用到了eval,所以运行效率较低,尝试用C语言又复刻了一遍。

如果只是为了得出一个Ture/False的结果的话,还是不算太复杂的,甚至不需要处理ZeroDivisionError的exception:

#include <math.h>
#include <stdio.h>

#define range(x, start, stop) for (int x = start; x < stop; x++)

float op(int type, float a, float b)
{
	switch (type) {
		case 0:	return a + b;
		case 1: return a - b;
		case 2: return b - a;
		case 3: return b * a;
		case 4: return a / b;
		case 5: return b / a;
	}
}

int calc(float *nums, int len, int target)
{
	float a, b;
	if (len == 1)
		return fabs(target - nums[0]) < 1e-4;
	range (i, 0, len) {
		range (j, i + 1, len) {
			float nums2[len - 1];
			int idx = 1;
			range (k, 0, len)
				if (k != i && k != j)
					nums2[idx++] = nums[k];
			range (k, 0, 6) {
				nums2[0] = op(k, nums[i], nums[j]);
				if (calc(nums2, len - 1, target))
					return 1;
			}
		}
	}
	return 0;
}

void main()
{
	int sum = 0, cnt = 0, min = 0, max = 9;
	range (n1, min, max + 1)
		range (n2, n1, max + 1)
			range (n3, n2, max + 1)
				range (n4, n3, max + 1) {
					float nums[4] = {n1, n2, n3, n4};
					sum++;
					cnt += calc(nums, 4, 24);
				}
	printf("%d/%d=%.2f%%\n", cnt, sum, 100.0 * cnt / sum);
}

运行时间22毫秒。

5.2 输出算式

如果需要打印计算式的话。。那就复杂很多了,因为C语言不原生支持eval、dict、hash、弹性列表、动态类型。。

Talk is cheap,直接上代码吧:

#include <math.h>
#include <stdio.h>
#include <string.h>

#define range(x, start, stop) for (int x = start; x < stop; x++)

typedef struct {
	int idx1, idx2, op;
} Stack;

typedef struct {
	float num;
	int op, span, left, right;
} Number;

void print_result(Stack *stack, float *nums, int len)
{
	// first initial
	Number number[len];
	memset(number, 0, sizeof(number));
	range (i, 0, len)
		number[i].num = nums[i];

	// simulate operate process
	range (i, 0, len - 1) {

		// stack info
		Stack st = stack[len-i-2];

		Number number2[len];
		memset(number2, 0, sizeof(number2));

		int idx = 0, idx1 = 0, idx2 = 0;

		// find true index1
		range (j, 0, st.idx1)
			idx1 += number[idx1].span + 1;
		st.idx1 = idx1;

		// first group of number
		number2[idx] = number[idx1];
		number2[idx].left++;
		number2[idx].span += number[st.idx2].span + 1;
		range (j, 0, number[st.idx1].span)
			number2[++idx] = number[++idx1];

		// operator
		number2[idx].op = st.op;

		// find true index2
		range (j, 0, st.idx2)
			idx2 += number[idx2].span + 1;
		st.idx2 = idx2;

		// second group of number
		number2[++idx] = number[idx2];
		range (j, 0, number[st.idx2].span)
			number2[++idx] = number[++idx2];
		number2[idx].right++;

		// remain numbers
		range (j, 0, len)
			if (!(st.idx1 <= j && j <= st.idx1 + number[st.idx1].span
				||st.idx2 <= j && j <= st.idx2 + number[st.idx2].span))
				number2[++idx] = number[j];

		// copy back
		range (i, 0, len)
			number[i] = number2[i];
	}

	// print result
	char op[6] = "?+*-/"; // set '?' at first for easy to debug
	range (i, 0, len) {
		range (j, 0, number[i].left)
			printf("(");
		printf("%g", number[i].num);
		range (j, 0, number[i].right)
			printf(")");
		if (i != len - 1)
			printf(" %c ", op[number[i].op]);
	}
	printf("\n");
}

float op(int type, float a, float b)
{
	switch (type) {
		case 1:	return a + b;
		case 2: return a * b;
		case 3: return a - b;
		case 4: return a / b;
	}
}

int calc(Stack *stack, float *nums, int len, int target)
{
	float a, b;
	if (len == 1)
		return fabs(target - nums[0]) < 1e-4;
	range (i, 0, len) {
		range (j, 0, len) {
			float nums2[len - 1];
			int idx = 1;
			range (k, 0, len)
				if (k != i && k != j)
					nums2[idx++] = nums[k];
			range (k, 1, 5) {
				if (i < j || i > j && k > 1) {
					nums2[0] = op(k, nums[i], nums[j]);
					if (calc(stack, nums2, len - 1, target)) {
						stack[len-2].idx1 = i;
						stack[len-2].idx2 = j;
						stack[len-2].op = k;
						return 1;
					}
				}
			}
		}
	}
	return 0;
}

void main()
{
	{
		Stack stack[4];
		float test[4] = {3, 3, 8, 8};
		if (calc(stack, test, 4, 24))
			print_result(stack, test, 4);
	}
	{
		Stack stack[6];
		float test[6] = {1, 1, 4, 5, 1, 4};
		if (calc(stack, test, 6, 24))
			print_result(stack, test, 6);
	}

	int sum = 0, cnt = 0, min = 0, max = 9;
	range (n1, min, max + 1)
		range (n2, n1, max + 1)
			range (n3, n2, max + 1)
				range (n4, n3, max + 1) {
					Stack stack[4];
					float nums[4] = {n1, n2, n3, n4};
					if (calc(stack, nums, 4, 24)) {
						print_result(stack, nums, 4);
						cnt++;
					}
					sum++;
				}
	printf("%d/%d=%.2f%%\n", cnt, sum, 100.0 * cnt / sum);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值