打表
打表是一种典型的用空间换时间的技巧,一般指将所有可能用到的结果事先计算出来,后面需要时直接查表获得。其常见的用法有以下几种:
- 在程序中一次性计算出所有后面可能遇到的结果,之后直接查询获取
- 在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中直接使用这些结果
- 在部分场合中,先暴力计算小规模数据的结果,然后通过找规律的方式解决问题
递推
在部分题目中找到合适的递推关系可能会极大降低题目的复杂度和解题时的时间复杂度
例题 【PAT B1040/A1039】有几个PAT
- 题目描述:
字符串APPAPT中包含了两个单词"PAT",其中第一个由2、4、6位单词组成,第二个由3、4、6位单词组成
现给定字符串,问一共可以形成多少个PAT - 输入格式:包含一行长度不超过105的字符串,只包括P、A、T这三个字母
- 输出格式:在一行中输出给定的字符串包含多少个PAT(对1000000007取余的结果)
- 题目分析:
若采取比较暴力的解法,在字符串较大时可能会超时。因此需要考虑采用其他思路解决问题。观察题目的目标字符串"PAT",对一个确定的A来说,其可以组成的PAT数目是其左侧P的数量和右侧T的数量的乘积,因此我们可以将问题转换成对字符串中每一个A左边的P和右边的T的数量的计算
对于P的数量,我们可以额外建立一个和输入字符串等长的整型数组leftP,其中存储字符串的对应位置左边P的数量(包括该位置本身)。从左到右遍历字符串,若当前位置是P则使leftP[i]的数值加一,若非P则继承leftP[i-1]的数值,如此获得整个leftP
而对于T的数量,也同样使用从右向左的遍历来获得。在这里也可以直接使用一个int变量rightT来记录右边T的数量,在遍历过程中遇到A时直接将rightT和对应位置的leftP[i]相乘,然后累加到计数变量ans中,直接得到答案。
#include<cstdio>
#include<cstring>
const int maxn = 100005;
const int MOD = 1000000007; //根据题目要求需要取余
char str_pat[maxn];//str_pat用于接收输入的字符串
int leftP[maxn] = {0};//起始将leftP置全0
int main() {
gets(str_pat);
int len = strlen(str_pat);
for (int i = 0; i < len; i++) {
if (i > 0) {
leftP[i] = leftP[i - 1];//非首位时先直接继承上一位结果
}
if (str_pat[i] == 'P') {
leftP[i]++;//检测到新的P时更新
}
}
int ans = 0, rightT = 0;
for (int i = len - 1; i >= 0; i--) {//逆向遍历,遇到T则更新rightT的值,遇到A则计算结果(注意取余)
if (str_pat[i] == 'T') {
rightT++;
} else if (str_pat[i] == 'A') {
ans = (ans + leftP[i] * rightT) % MOD;
}
}
printf("%d\n",ans);
return 0;
}
随机选择算法
引例问题:如何在一个无序数组中求出第K大的数
比较直观的想法是先将数组排序,然后直接取出第K个元素。但实际上还可以对这个方法进行优化——即使用随机选择算法
随机选择算法原理与快速排序相似,对序列使用一次和快速排序中的划分后,设主元为p。随机选择的主元p左侧的元素都小于主元,右侧元素都大于主元,因此主元在序列中的排序M已经确定。若 M = K ,则直接取p为题目结果,否则根据M和K的大小关系向主元p的左侧区间或右侧区间递归划分,直到满足 M’ = K为止或达到递归边界
由此可以得到随机选择算法:
int RandPartition(int A[], int left, int right) {
int p = round(1.0 * rand()/RAND_MAX * (right - left) + left);
swap(A[p], A[left]);
int temp = A[left];
while (left < right) {
while (left < right) && A[right] > temp) right--;
A[left] = A[right];
while (left < right) && A[left] < temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
int randSelect(int A[], int left, int right, int K) {
if (left == right) return A[left];//递归边界
int p = RandPartition(int A[], int left, int right);//通过randPartition方法获得主元
int M = p - left + 1;//获取当前主元在当前区间的排位
if (K == M) return A[p];//排位对应相等则直接返回
if (K < M) return randSelect(A, left, p-1, K);//向主元左侧区间继续递归寻找
else return randSelect(A, p+1, right, K); //向主元右侧继续递归寻找
}
例题
- 问题描述:给定一个由整数组成的集合,集合中的整数各不相同,现欲将集合分为两个子集合,使得两个子集合的并为原集合,交为空集,同时两个子集合的元素个数n1和n2的差的绝对值|n1-n2|尽可能小的前提下,要求它们各自的元素之和S1和S2之差的绝对值|S1-S2|尽可能大,求这个|S1-S2|的值
- 题目分析:
为了使|n1-n2|尽可能小,则划分出的两个子集合的元素数应当尽可能的接近。因此当n为偶数时,n1和n2取值都为n/2;当n为奇数时,一个是n/2,另一个是n/2+1。在这个情况下,想要使|S1-S2|尽可能大,那么在选取子集合元素时,一个尽可能选择较小的元素,另一个尽可能选择较大的元素即可满足题目要求。而此时就可以利用前述的随机选择算法尽快找到位于n/2的元素并完成序列的划分,从而直接计算得出结果
#include<cstdio>
#include<cstdlib>
#include<ctime>
#include<algorithm>
using namespace std;
const int maxn = 100005;//根据题目具体要求设置
int A[maxn], n;
//随机主元的划分方法
int RandPartition(int A[], int left, int right) {
int p = round(1.0 * rand()/RAND_MAX * (right - left) + left);
swap(A[p], A[left]);
int temp = A[left];
while (left < right) {
while (left < right) && A[right] > temp) right--;
A[left] = A[right];
while (left < right) && A[left] < temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
//随机选择算法
int randSelect(int A[], int left, int right, int K) {
if (left == right) return A[left];
int p = RandPartition(int A[], int left, int right);
int M = p - left + 1;
if (K == M) return A[p];
if (K < M) return randSelect(A, left, p-1, K);
else return randSelect(A, p+1, right, K - M);
}
int main() {
srand((unsigned)time(NULL));//初始化随机数种子
int sum1 = 0, sum2 = 0;//sum1存储序列总和,sum2存储划分以后前n/2个元素之和
scanf("%d",&n);
for (int i = 0; i < n; i++) {
scanf("%d",&A[i]);
sum1 += A[i];
}
randSelect(A, 0, n-1, n/2);
for (int i = 0; i < n/2; i++) {
sum2 += A[i];
}
printf("%d\n", (sum1-sum2)-sum2);
}