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);
}