题目描述
对于输入的一个正整数n,我们可以进行下面三种操作:
- 减1 操作:n :=n-1。
- 除以2 操作:n := n/2,要求执行前n 是2 的倍数。
- 除以3 操作:n := n/3,要求执行前n 是3 的倍数。
请计算将n 变为1 最少需要多少个操作。
示例:
输入:17
输出:5
解释:1*2*2*2*2+1=17,总共经过至少5次操作完成从1到17的变换。
思路
方法1:暴力递归
以数组arr[i]
表示从1到i的最小“操作数”。利用函数递归,暴力地从1到n列举所有的可能性,更新数组取值,取a[n]
即可。
#include <iostream>
using namespace std;
#define MAX 1000000000
class Solution
{
public:
int minOperations(int n)
{
Init(n);
arr[1] = 0;
Operate(0, 1); //从1开始进行递归求解,直到n截至
return arr[n];
}
private:
int target;
int arr[10001];
void Init(int n) //初始化数组arr和最终“目标”target
{
for (int i = 0; i < 10001; i++)
{
arr[i] = MAX;
}
target = n;
}
void Operate(int pre, int cur) //表示由pre到cur可通过“一步”操作得到
{
if (cur > target) //如果当前递归到的值大于n,则没有必要继续进行下去了。
return;
arr[cur] = min(arr[cur], arr[pre] + 1); //更新当前最小“操作数”
//递归求解所有可能
Operate(cur, cur * 3);
Operate(cur, cur * 2);
Operate(cur, cur + 1);
}
};
int main()
{
Solution s;
cout << s.minOperations(17) << endl;
}
运行结果:
5
但是上面的做法时间复杂度显然是 O ( n 3 ) O(n^3) O(n3),当数据较大时,时间代价是无法接受的。
方法2:动态规划
假设dp[i]
表示从1到n的最小“操作数”,我们显然有如下状态转移方程:
d
p
[
i
]
=
{
d
p
[
i
−
1
]
+
1
,
n
∤
2
,
n
∤
3
m
i
n
{
d
p
[
i
−
1
]
,
d
p
[
i
/
2
]
}
+
1
,
n
∣
2
,
n
∤
3
m
i
n
{
d
p
[
i
−
1
]
,
d
p
[
i
/
3
]
}
+
1
,
n
∤
2
,
n
∣
3
m
i
n
{
d
p
[
i
−
1
]
,
d
p
[
i
/
2
]
,
d
p
[
i
/
3
]
}
+
1
,
n
∣
2
,
n
∣
3
dp[i]= \begin{cases} dp[i-1]+1,& \text{$n \nmid 2 ,n \nmid3$}\\ min\{dp[i-1],dp[i/2]\}+1,& \text{$n\mid2,n\nmid 3$}\\ min\{dp[i-1],dp[i/3]\}+1,& \text{$n\nmid2,n\mid 3$}\\ min\{dp[i-1],dp[i/2],dp[i/3]\}+1,&\text{$n\mid2,n\mid3$} \end{cases}
dp[i]=⎩⎪⎪⎪⎨⎪⎪⎪⎧dp[i−1]+1,min{dp[i−1],dp[i/2]}+1,min{dp[i−1],dp[i/3]}+1,min{dp[i−1],dp[i/2],dp[i/3]}+1,n∤2,n∤3n∣2,n∤3n∤2,n∣3n∣2,n∣3
且有
d
p
[
1
]
=
0
,
d
p
[
2
]
=
1
,
d
p
[
3
]
=
1
dp[1]=0,dp[2]=1,dp[3]=1
dp[1]=0,dp[2]=1,dp[3]=1。
所以可以通过遍历一遍
d
p
dp
dp数组得到解,时间复杂度为
O
(
n
)
O(n)
O(n)!
代码如下:
#include <iostream>
#include <assert.h>
using namespace std;
#define MAX 1000000000
class Solution
{
public:
int minOperations(int n)
{
assert(n > 0); //注意n是大于0的正整数
int *dp = new int[n + 1]; //注意数组下标
for (int i = 1; i < n + 1; i++)
dp[i] = MAX;
dp[1] = 0;
dp[2] = 1;
dp[3] = 1;
if (n >= 4)
{
for (int i = 4; i <= n; i++)
{
if (i % 2 != 0 && i % 3 != 0) // i不整除2也不整除3
dp[i] = dp[i - 1] + 1;
else if (i % 2 == 0 && i % 3 != 0)
dp[i] = min(dp[i - 1], dp[i / 2]) + 1;
else if (i % 3 == 0 && i % 2 != 0)
dp[i] = min(dp[i - 1], dp[i / 3]) + 1;
else if (i % 3 == 0 && i % 2 == 0)
dp[i] = min(dp[i - 1], min(dp[i / 2], dp[i / 3])) + 1;
}
}
return dp[n];
}
};
int main()
{
Solution s;
cout << s.minOperations(6561) << endl;
}
执行上面的样例,并使用time
命令测试运行时间,输出结果为:
可以看到,快还是
O
(
n
)
O(n)
O(n)快啊~,上面
O
(
3
n
)
O(3^n)
O(3n)暴力算法只要
n
n
n的大小超过1000,就会迟迟得不到结果。