sysu2023大一期中考压轴题:计算f(x)

题目:

请实现一个程序计算f(x)
f(x)和g(x)的定义如下:
x < 10 x<10 x<10
{ f ( x ) = 1 g ( x ) = 1 \begin{cases} \begin{aligned} f(x)&=1\\ g(x)&=1 \end{aligned} \end{cases} {f(x)g(x)=1=1
x ∈ [ 10 , 10000 ) x\in[10,10000) x[10,10000)
{ f ( x ) = f ( x − 1 ) g ( x − 2 ) + f ( x − 2 ) g ( x − 1 ) g ( x ) = f ( ⌊ x 2 ⌋ ) g ( x − 1 ) \begin{cases} \begin{aligned} f(x)&=f(x-1)g(x-2)+f(x-2)g(x-1)\\ g(x)&=f(\lfloor \frac x 2\rfloor)g(x-1) \end{aligned} \end{cases} f(x)g(x)=f(x1)g(x2)+f(x2)g(x1)=f(⌊2x⌋)g(x1)
因为结果可能会很大,请输出f(x)对10007取余的结果
样例输入:20
样例输出:233

解法1:直接递归求解

#include<iostream>
using namespace std;
using ll = long long;
#define mod 10007
ll g(ll x);
ll f(ll x)//函数f(x)
{
	if (x < 10)
		return 1;
	return f(x - 1) * g(x - 2) + f(x - 2) * g(x - 1);
}
ll g(ll x)//函数g(x)
{
	if (x < 10)
		return 1;
	return f(x / 2) * g(x - 1);
}
int main()
{
	int x;
	cin >> x;
	cout << f(x) % mod << endl;
	return 0;
}

这段代码能通过样例测试,但是当x取30时输出了一个负数,很明显是f(30)的值过大导致long long溢出
在这里插入图片描述
如何处理该问题?要知道long long是C++最长的整数类型,x等于30时就已经越界了,更不必说x=10000
一种思路是实现一个BigInt大整数类,用字符串存储和计算整数,代码比较复杂,因为需要重载实现加法、乘法和取余运算,不知道有没有大佬用这种方法解出此题。

解法2:数学+记忆化递归

递归的过程中f(x)一直在做乘法而快速增长,为了避免f(x)过大,能否在递归f(x)的过程中进行取余运算?
以下先证明两个定理:
注:%为取余运算

引理1: ( a ⋅ b ) % m = ( ( a % m ) ⋅ ( b % m ) ) % m (a\cdot b)\%m=((a\%m)\cdot(b\% m))\%m (ab)%m=((a%m)(b%m))%m

证明:


{ a = m a 1 + a 2 b = m b 1 + b 2 ( m , a 1 , a 2 , b 1 , b 2 ∈ N + ) \begin{cases} a=ma_1+a_2\\ b=mb_1+b_2\\ \end{cases} (m,a_1,a_2,b_1,b_2\in N^+) {a=ma1+a2b=mb1+b2(m,a1,a2,b1,b2N+)

满足
a % m = a 2 b % m = b 2 a\%m=a_2\\ b\%m=b_2 a%m=a2b%m=b2

a ⋅ b = m 2 a 1 b 1 + m ( a 1 b 2 + a 2 b 1 ) + a 2 b 2 a\cdot b=m^2a_1b_1+m(a_1b_2+a_2b_1)+a_2b_2 ab=m2a1b1+m(a1b2+a2b1)+a2b2
因为
( m 2 a 1 b 1 + m ( a 1 b 2 + a 2 b 1 ) ) % m = 0 (m^2a_1b_1+m(a_1b_2+a_2b_1))\%m=0 (m2a1b1+m(a1b2+a2b1))%m=0
所以
( a ⋅ b ) % m = ( a 2 b 2 ) % m = ( ( a % m ) ⋅ ( b % m ) ) % m \begin{aligned} (a\cdot b)\%m&=(a_2b_2)\%m\\ &=((a\%m)\cdot(b\% m))\%m \end{aligned} (ab)%m=(a2b2)%m=((a%m)(b%m))%m

引理2: ( a + b ) % m = ( a % m + b % m ) % m (a+b)\%m=(a\%m+b\%m)\%m (a+b)%m=(a%m+b%m)%m

证明:
{ a = m a 1 + a 2 b = m b 1 + b 2 ( m , a 1 , a 2 , b 1 , b 2 ∈ N + ) \begin{cases} a=ma_1+a_2\\ b=mb_1+b_2\\ \end{cases} (m,a_1,a_2,b_1,b_2\in N^+) {a=ma1+a2b=mb1+b2(m,a1,a2,b1,b2N+)

a % m = a 2 b % m = b 2 a\%m=a_2\\ b\%m=b_2 a%m=a2b%m=b2

满足:
a + b = m ( a 1 + b 1 ) + a 2 + b 2 a+b=m(a_1+b_1)+a_2+b_2 a+b=m(a1+b1)+a2+b2

( m ( a 1 + b 1 ) ) % m = 0 (m(a_1+b_1))\%m=0 (m(a1+b1))%m=0

所以:
( a + b ) % m = ( a 2 + b 2 ) % m = ( a % m + b % m ) % m \begin{aligned} (a+b)\%m&=(a_2+b_2)\%m\\ &=(a\%m+b\%m)\%m \end{aligned} (a+b)%m=(a2+b2)%m=(a%m+b%m)%m

思路和代码:

(mod=10007)
要求f(x)%mod,等价于
(f(x-1)g(x-2)+f(x-2)g(x-1))%mod
利用上述两个引理,这等价于
(((f(x - 1) % mod) * (g(x - 2) % mod) % mod) + ((f(x - 2) % mod) * (g(x - 1) % mod))%mod) % mod
同理,g(x)%mod等价于
((f(x / 2)%mod) *( g(x - 1)%mod))%mod
这样,可以保证f(x)和g(x)都小于mod,不会溢出
可以这样改进代码:

#include<iostream>
using namespace std;
using ll = long long;
#define mod 10007
ll g(ll x);
ll f(ll x)//函数f(x)
{
	if (x < 10)
		return 1;
	return (((f(x - 1) % mod) * (g(x - 2) % mod) % mod) + ((f(x - 2) % mod) * (g(x - 1) % mod))%mod) % mod;
}
ll g(ll x)//函数g(x)
{
	if (x < 10)
		return 1;
	return ((f(x / 2) % mod) * (g(x - 1) % mod)) % mod;
}
int main()
{
	int x;
	cin >> x;
	cout << f(x) % mod << endl;
	return 0;
}

这样就解决了溢出问题,输入30不会出现负数,但输入100时就程序超时了

复杂度分析1:

请添加图片描述
递归的过程相当于一次深度优先搜索DFS,从f(x)递归到f(k)和g(k)满足k<10,每一个元素都会引出常数m个分支,每递归一层,层内元素的个数就会增大m倍,总共递归x-8层(x很大时-8忽略不计),所以时间复杂度 O ( m x ) O(m^x) O(mx) (m为常数)
由于最大递归x-8层,空间复杂度为 O ( x ) O(x) O(x)
x很大时,递归层数很高,导致出现大量重复的运算,会消耗大量时间,例如上图中g(x-2)重复出现了三次,采用记忆化递归可以避免重复计算.

记忆化递归

可以用unordered_map<int,int>哈希表hashfhashg来存储已经计算过的f(x)和g(x)值,利用之前计算过的结果减少递归。
AC代码:

#include<iostream>
#include<unordered_map>
using namespace std;
using ll=long long;
#define mod 10007
unordered_map<int, ll> hashf, hashg;
ll g(ll x);
ll f(ll x)//函数f(x)
{
	if (x < 10)
		return 1;
	ll a, b, c, d;//分别代表(f(x - 1)%mod)、 (g(x - 2)%mod) 、 (f(x - 2)%mod) 、 (g(x - 1)%mod)
	if (hashf[x - 1] != 0)
	{
		a = hashf[x - 1];
	}
	else
	{
		a = f(x - 1) % mod;
		hashf[x - 1] = a;
	}
	if (hashg[x - 2] != 0)
	{
		b = hashg[x - 2];
	}
	else
	{
		b = g(x - 2) % mod;
		hashg[x - 2] = b;
	}
	if (hashf[x - 2] != 0)
	{
		c = hashf[x - 2];
	}
	else
	{
		c = f(x - 2) % mod;
		hashf[x - 2] = c;
	}
	if (hashg[x - 1] != 0)
	{
		d = hashg[x - 1];
	}
	else
	{
		d = g(x - 1) % mod;
		hashg[x - 1] = d;
	}
	return ((a * b) % mod + (c * d) % mod) % mod;
}
ll g(ll x)//函数g(x)
{
	if (x < 10)
		return 1;
	ll a, b;
	if (hashf[x /2] != 0)
	{
		a = hashf[x /2];
	}
	else
	{
		a = f(x/2) % mod;
		hashf[x /2] = a;
	}
	if (hashg[x - 1] != 0)
	{
		b = hashg[x - 1];
	}
	else
	{
		b = g(x - 1) % mod;
		hashg[x - 1] = b;
	}
	return (a *b)%mod;
}
int main()
{
	int x;
	cin >> x;
	cout << f(x) % mod << endl;
	return 0;
}

复杂度分析2:

请添加图片描述
该过程同为深度优先搜索,不同的是增加了很多递归的终点(黄色和绿色元素)

  1. 黄色元素为递归到x<10得到结果,结束递归
  2. 绿色元素因为利用了存储在哈希表中的之前计算过的结果,也是递归的终点
  3. 红色元素的计算结果存入哈希表,供之后使用.
    在递归的每一层,都只用处理常数m个分支,总共递归x-8层,所以时间和空间复杂度为 O ( m ( x − 8 ) ) O(m(x-8)) O(m(x8)) O ( x ) O(x) O(x)
    用Visual Studio跑这段代码x=10000可能会Stack overflow (VS的函数调用栈比较小,会溢出,不知有没有大佬会解决)
    博主考试时这样写,是可以通过的,用devC++或matrix平台都可以正常运行

解法3:数学+循环代替递归

感谢chenwn11提供的思路,可以用数组储存f(x)和g(x)对mod取余后的结果,从小到大循环计算f(x)和g(x),不使用函数调用栈,可以防止爆栈

#include<iostream>
#include<vector>
#define mod 10007
using ll = long long;
using namespace std;
int main()
{
	int x;
	cin >> x;
	if (x < 10)
	{
		cout << 1 << endl;
		return 0;
	}
	vector<ll> f(x+1), g(x+1);
	for (int i = 0; i < 10; ++i)
	{
		f[i] = 1;
		g[i] = 1;
	}
	for (int i = 10; i <= x; ++i)
	{
		f[i] = ((f[i - 1] * g[i - 2]) % mod + (f[i - 2] * g[i - 1]) % mod) % mod;
		g[i] = (f[i / 2] * g[i - 1]) % mod;
	}
	cout << f[x] << endl;
	return 0;
}

这段代码时间和空间复杂度: O ( x ) O(x) O(x) ,可以用VS正常运行
各位大佬如果有更好的思路欢迎提出

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值