打表
- 在程序中一次性计算出所有需要用到的结果,之后的查询直接取出这些结果
- 在程序B中分一次或者多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后再程序A中可以直接使用这些结果
- 对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许能够发现一些规律
活用递推
考虑递推关系可能会使时间复杂度降低。
如与序列有关的题目,假如序列的每一位所需要计算的值都可以通过该位左右两侧的结果计算得到,那么可以考虑所谓的得“左右两侧的结果”能否通过递推进行预处理得到,这样在后面的使用中就可以不必反复求解。
题目:有几个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
思路:
要想知道构成多少个PAT,那么遍历字符串后对于每一A,它前面的P的个数和它后面的T的个数的乘积就是能构成的PAT的个数。然后把对于每一个A的结果相加即可;所以,先利用一个数组遍历每一位左边有几个P(含当前位),从左往右,若当前位i是P,则leftNumP[i] = leftNumP[i-1] + 1;若当前位i不是P,则leftNumP[i] = leftNumP[i-1]。
同样,计算出每一位右边T的个数。
#include<cstdio>
#include<cstring>
const int MAXN = 100050;
const int MOD = 1000000007;
int main(){
char str[MAXN];
int leftNumP[MAXN] = {0};
gets(str); // 输入字符串
int len = strlen(str); // 字符串长度
// 先统计P
for(int i = 0; i < len; i++){
if(i > 0) leftNumP[i] = leftNumP[i - 1];
if(str[i] == 'P') leftNumP[i]++;
}
int ans = 0;
int countT = 0;
// 统计T
for(int i = len - 1; i >= 0; i--){
if(str[i] == 'T'){
countT++;
}else if(str[i] == 'A'){
ans = (ans + countT * leftNumP[i]) % MOD;
}
}
printf("%d
", ans);
return 0;
}
随机选择算法
如何从一个无序的数组中求出第k大
的数。这个问题最直接的想法是对数组排一下序,然后直接取出第k个元素即可,这样做法需要O(nlogn)
的时间复杂度。(这个方法比较简单,在运行时间允许的情况下当然选这个方法)
下面介绍随机选择算法,它对任何输入都可以达到O(n)
的期望时间复杂度。
基本思想:随机选择算法的原理类似于随机快速排序算法
。当对A[left,right]
执行一次randPartition()
函数之后,主元左侧的元素个数就是确定的,且它们都小于主元。假设此时主元是A[p]
,那么A[p]
就是A[left,right]
中的第p-left+1
大的数。不妨令M
表示p-left+1
,那么如果k==M
成立,说明第k大
的数就是主元A[p]
;如果k<M
成立,就说明第k大
的数在主元右侧
,即A[(p+1)...right]
中的第k-M大
,往右侧递归即可。算法以left==right
作为递归边界,返回A[left]
。由此可以写出随机选择算法代码:
void randSelect(int A[], int left, int right, int K){
if(left == right) return A[left]; · // 边界
int p = randPartition(A, left, right); // 划分后主元的位置为P
int M = p - left + 1; // A[p]是A[left,right]中的第M大
if(K == M) return A[p]; // 找到第K大的数
if(K < M){ // 第K大的数在主元左侧
return randSelect(A, left, p - 1, K); // 往主元左侧找第K大
}else{ // 第K大的数在主元右侧
return randSelect(A, p + 1, right, K - M); // 往主元右侧找第K-M大
}
}
可以证明,虽然随机选择算法的最坏时间复杂度是O(n²)
,但是其对任意输入的期望时间复杂度却是O(n)
,这意味着不存在一组特定的数据能使这个算法达到最坏情况,是个相当实用和出色的算法。
应用
给定一个由整数组成的集合,集合中的整数各不相同,现在要将它分为两个子集合,使得这两个子集合的并为原集合,交为空,同时在这两个子集合的元素个数n1和n2之差的绝对值尽可能小的情况下,它们各自的元素之和S1和S2之差的绝对值尽可能大。求|S1-S2|的值。
解题思路
如果有偶数个,则分出的两个子集合都是n/2
如果有奇数个,则分出的两个子集合分别是n/2 和 n/2 + 1
方法一:
将元素从小到大排序,将前n/2个作为一个子集合,剩下的为一个子集合,时间复杂度为O(nlogn)
方法二:
利用上诉的随机选择算法。
使用randSelect函数求出第n/2大的数即可,该函数会自动切分好两个集合,期望时间复杂度为O(n)
代码
#include<stdio.h>
#include<time.h>
#include<algorithm>
#include<stdlib.h>
#include<math.h>
using namespace std;
const int MAXN = 100010;
// 选择随机主元,对区间[left,right]进行划分
int randPartition(int A[], int left, int right){
int randP = (int)(round(1.0*rand() / RAND_MAX * (right - left) + left));
swap(A[randP], A[left]);
int temp = A[left];
while(left < right){
while(right > temp) right--;
A[left] = A[right];
while(left < temp) left++;
A[right] = A[left];
}
A[left] = temp;
return left;
}
// 随机选择算法
void randSelect(int A[], int left, int right, int K){
if(left == right) return;
int position = randPartition(A, left, right);
int M = position - left + 1;
if(M == K) return;
if(M < K) randSelect(A, left, position - 1, K);
else randSelect(A, position + 1, right, K - M);
}
int main(){
int n;
int A[MAXN] = {0};
srand(unsigned)time(NULL);
int sum = 0, sum_remain = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d", &A[i]);
sum += A[i];
}
randSelect(A, 0, n - 1, n / 2);
for(int i = 0; i < n / 2; i++){
sum_remain += A[i];
}
printf("%d
", (sum - sum_remain) - sum_remain); // 求两个子集合之差
return 0;
}
Write by Gqq