关于经典打靶问题的研究
这个问题是:一共10环的靶,打10枪中90环,一共有多少种打法。
最直观能想到的就是穷举:
int count = 0;
for (int i1 = 0; i1 <= 10; i1++)
for (int i2 = 0; i2 <= 10; i2++)
for (int i3 = 0; i3 <= 10; i3++)
for (int i4 = 0; i4 <= 10; i4++)
for (int i5 = 0; i5 <= 10; i5++)
for (int i6 = 0; i6 <= 10; i6++)
for (int i7 = 0; i7 <= 10; i7++)
for (int i8 = 0; i8 <= 10; i8++)
for (int i9 = 0; i9 <= 10; i9++)
for (int i10 = 0; i10 <= 10; i10++)
if (i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9 + i10 == 90)
{
Console.WriteLine("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}", i1, i2, i3, i4, i5, i6, i7, i8, i9, i10);
count++;
}
for (int i1 = 0; i1 <= 10; i1++)
for (int i2 = 0; i2 <= 10; i2++)
for (int i3 = 0; i3 <= 10; i3++)
for (int i4 = 0; i4 <= 10; i4++)
for (int i5 = 0; i5 <= 10; i5++)
for (int i6 = 0; i6 <= 10; i6++)
for (int i7 = 0; i7 <= 10; i7++)
for (int i8 = 0; i8 <= 10; i8++)
for (int i9 = 0; i9 <= 10; i9++)
for (int i10 = 0; i10 <= 10; i10++)
if (i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + i9 + i10 == 90)
{
Console.WriteLine("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9}", i1, i2, i3, i4, i5, i6, i7, i8, i9, i10);
count++;
}
Console.WriteLine(count);
但这是很慢的一种做法。
其实有很多组合,通过直接计算就能提前判断出能不能中90环,比如头几枪环数很低,后面就算每枪十环也到不了90环,那后面几枪都不用打了;或者10枪都中10环。出现这些情况之后。
要把这些判断加到每一枪(也就是上面代码的每一层循环),明显重复代码太多。所以可以用递归,把每一层循环提取出来。
这是递归版本的代码 (参考了网上一些现成的代码):
class Program
{
static void Main(string[] args)
{
elements = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //定义每一枪可能中的环数。
combinations = new int[10]; //定义一共要打几枪。这个数组会保存每一种打中90环的方法中每一枪的环数。
count = 0;
finalResult = 90; //定义最终需要的环数。如果这里是99,那实际上只有一种组合。
Compute(combinations.Length, 0, combinations);
Console.WriteLine(count);
Console.WriteLine(actualCount);
}
static int finalResult;
static int count;
static int actualCount = 0;
static List<int> elements;
static int[] combinations;
static void Compute(int step, int total, int[] datas)
{
count++;
if (total > finalResult || total + elements[elements.Count - 1] * (step) < finalResult)
{
return;
}
if (step == 0)
{
if (total == finalResult)
{
string result = "";
for (int i = 0; i < datas.Length; i++)
{
result += datas[i].ToString() + " ";
}
Console.WriteLine(count.ToString() + " " + result);
actualCount++;
}
return;
}
for (int i = 0; i < elements.Count; i++)
{
datas[step - 1] = elements[i];
Compute(step - 1, total + elements[i], datas);
}
}
{
static void Main(string[] args)
{
elements = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //定义每一枪可能中的环数。
combinations = new int[10]; //定义一共要打几枪。这个数组会保存每一种打中90环的方法中每一枪的环数。
count = 0;
finalResult = 90; //定义最终需要的环数。如果这里是99,那实际上只有一种组合。
Compute(combinations.Length, 0, combinations);
Console.WriteLine(count);
Console.WriteLine(actualCount);
}
static int finalResult;
static int count;
static int actualCount = 0;
static List<int> elements;
static int[] combinations;
static void Compute(int step, int total, int[] datas)
{
count++;
if (total > finalResult || total + elements[elements.Count - 1] * (step) < finalResult)
{
return;
}
if (step == 0)
{
if (total == finalResult)
{
string result = "";
for (int i = 0; i < datas.Length; i++)
{
result += datas[i].ToString() + " ";
}
Console.WriteLine(count.ToString() + " " + result);
actualCount++;
}
return;
}
for (int i = 0; i < elements.Count; i++)
{
datas[step - 1] = elements[i];
Compute(step - 1, total + elements[i], datas);
}
}
}
用这种方法,只需要判断1847561次,比起第一种方法要判断10000000000次,确实性能提高不少。
根据一般研究算法的套路:
第一步,写出一个直观算法,代码很直观,但算法复杂度超级高;
第二步,写出一个优化过的算法(可能通过一些数学或逻辑技巧等优化),性能达到以可以接受的范围,但相应的代码可读性降低不少;
第三步,通过数学技巧(如数学归纳法)对算法进行建模,得出公式,最终得出时间复杂度为1的算法。
相信对于这个打靶问题,也是存在一个公式的,不过我的数学不好,不知道有没有牛人能提供一个解决方案。