打表与活用递推

本篇博客主要介绍两个编程技巧:一个是打表,一个则是活用递推

1、打表

打表是一种典型的用空间换时间的技巧,是指将所有可能需要用到的结果事先计算出来,这样后面需要用到时就可以直接查表获得。打表常见的用法有如下几种:

  • 在程序中一次性计算处所有需要用到的结果,之后的查询直接取这些结果
    这个是最常用到的用法,例如在一个需要查询大量Fibonacci数F(n)的问题中,显然每次从头开始计算是非常耗时的,如果要Q次查询的话显然就会产生O(nQ)的时间复杂度;而如果我们打表来实现的话,即先进行预处理,把所有Fibonacci数预先计算并存到一个数组中,那么每次查询就只需要O(1)的时间复杂度,那么Q次查询的话就是O(Q)的时间复杂度,再加上预处理的O(n)时间复杂度,显然打表后最终的时间复杂度最终是O(n+Q),从 nQ 到 n+Q 时间复杂度显然大大改进!

  • 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果
    这种用法一般是当程序一部分过程消耗的时间过多,或是没有想到好的算法,因此考虑在另一个程序中使用暴力算法求出结果,这样就能直接在原程序中使用这些结果。例如对n皇后问题来说,如果使用的算法不够好(或是用的组合数枚举,或是用的暴力枚举),那么就很容易超时。因此我们考虑在本地中用程序计算出对所有n来说n皇后问题的方案数,然后把算出的结果直接写在数组中,就可以根据题目输入的n来直接输出结果。

  • 对于一些感觉没啥思路的题目,可以考虑先用暴力程序计算小范围数据的结果,然后找规律,或许就能“柳暗花明”
    这种用法在数据范围非常大时容易用到,因为这样的题目可能不是直接用想到的算法来解决的,而 是需要先小范围的打表再寻找一些规律才能AC掉。

2、活用递推

有很多题目都需要考虑一下过程中是否可能存在递推关系,如果能找到这样的递推关系,显然就能大大降低时间复杂度。例如就一类涉及序列的题目来说,加入序列的每一位所需要计算的值都可以通过该位左右两侧的结果计算得到,那么就可以考虑所谓的“左右两侧的结果”是否能通过递推进行预处理来得到,这样在后面的使用中就可以不必反复求解。

下面来看一道对应的例题:

【PAT B1040/A1093】有几个PAT

题目描述:
字符串 APPAPT 中包含了两个单词 PAT,其中第一个 PAT 是第 2 位( P ),第 4 位(A),第 6 位(T);第二个 PAT 是第 3 位( P ),第 4 位(A),第 6 位(T)。
现给定字符串,问一共可以形成多少个 PAT?
输入格式:
输入只有一行,包含一个字符串,长度不超过10​5​​,只包含 P、A、T 三种字母。
输出格式:
在一行中输出给定字符串中包含多少个 PAT。由于结果可能比较大,只输出对 1000000007 取余数的结果。
输入样例:
APPAPT
输出样例:
2

分析:
直接暴力显然会超时。
那么我们不妨换个角度思考一下这个问题,对一个确定的位置A来说,以它形成的PAT的个数等于它左边的P的个数乘以它右边T的个数。例如对字符串APPAPT来说,它左边有两个P,右边有一个,因此这个A能形成的PAT的个数就是2.于是这道问题就转换成,对于字符串中的每个A,计算它左边P的个数与它右边T的个数的乘积,然后把所有A的这个乘积相加便得到本题的答案。
那么怎么比较快地获得每一位左边P的个数的方法呢?
可以考虑设定一个数组leftNumP,记录每一位左边P的个数(含当前位,下同)。接着从左到右遍历字符串,如果当前位 i 是P,那么leftNumP[i] = leftNumP[i-1] + 1 ;如果当前位 i 不是P,那么leftNumP[i] = leftNumP[i-1]。那么显然只需要O(len)的时间复杂度就能统计出leftNumP数组。
以同样的方法可以计算出每一位右边T的个数。
为了节省代码量,可以在统计每一位右边T的个数的时候直接计算出答案ans。具体做法是:定义一个变量rightNumT,记录当前累计右边的个数。从左往右遍历字符串,如果当前位 i 是T,那么令rightNumT+1;否则如果当前位 i 是A,那么令ans + leftNumP[i]*rightNumT(注意取模)。这样,当遍历按字符串时,就得到了答案ans。

给出代码:

#include<cstdio>
#include<cstring>
const int MAXN = 100010;
const int MOD = 1000000007;
char str[MAXN];//字符串 
int leftNumP[MAXN] = {0};//每一位左边含P的个数 
int main(){
	gets(str);//读入字符串 
	int len = strlen(str);//长度 
	for(int i = 0;i<len;i++){//从左到右遍历字符串 
		if(i>0){//如果不是0号位 
			leftNumP[i] = leftNumP[i-1];//继承上一位的结果 
		}
		if(str[i] == 'P'){//当前位是P 
			leftNumP[i]++; //leftNump[i]+1
		} 
	}
	int ans = 0,rightNumT = 0;//ans为答案,rightNumT记录右边T的个数 
	for(int i = len-1;i>=0;i--){
		if(str[i] == 'T'){
			rightNumT++;//当前位是T,A右边T的个数+1 
		}else if(str[i] == 'A'){
			ans = (ans + leftNumP[i]*rightNumT)%MOD;
		}
	} 
	printf("%d\n",ans);
	return 0;
}

总结

掌握好这两种技巧对于解决问题有着重要的意义,需多做题目,仔细领会,在做题目中掌握技巧

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值