这里引用一下官方题解
考察点:动态规划
可以使用暴力搜索来获取所有的方案数,但是如同斐波那契数列一样,数据一大就不能计算,因为我们重复算了很多次相同的内容.
改用递推法求解,设d[i][j]为从(1,1)走到(i,j)的方案数
d[1][1]=1
d[i][j]+= d[i-1][j] if(s[i-1][j]=='B' || s[i-1][j]=='D')
d[i][j]+= d[i][j-1] if(s[i-1][j]=='B' || s[i-1][j]=='R')
最后d[n][m]就是答案.
经典中的经典算法:动态规划
kuangbin动态规划入门专题
考察点:构造
需要知道一个知识点:任意的非负整数都可以由若干互不相同的2的次幂相加得到,剩下的就按官方照题解的构造方式把这些2次幂得到然后根据输入进行拼接。
考察点:模拟
注意细节,不要只判断(x,y)中x<0或者y<0,因为x>=n 或者 y>=m时,也是属于Undefined Behaviour。
同样判断m*x+y时,不要只判断是否是负数,当>=n*m时同样属于Runtime error。
考察点:二叉树的数组存储方式
实际上这道题比过的比较多的2个还要简单一些只是做的人比较少,在比赛中榜单也只是参考不能排除"歪榜"的情况。
存储方式题目上已经介绍的比较清楚了若当前节点为i则,左儿子为i*2,右儿子为i*2+1,父节点为i/2。
因为最后要按照节点数字顺序进行输出,所以我们在接受时可以再开一个数组来记录每个数字的所在位置如:if (a[i] != -1) pos[a[i]] = i,最后注意判断节点编号是否超出n是否小于1。
考察点:期望、贡献
因为是异或运算我们考虑每个二进制位的贡献,在这道题里的期望实际上就是每个二进制位权*这个二进制位出现的概率。
对于一个二进制位w来说,如果他在异或结果中存在那么只有两种情况:在a中存在且b中不存在、在a中不存在且在b中存在。
设A中存在二进制位w的概率为Pa,因为是随机抽取所以出现概率为区间[l1, r1]带有这个二进制位的数字数量/区间[l1, r1]长度,同样也可以求出Pb。
最后枚举w为1、2、4、8···的二进制位权,对答案的贡献为w*(Pa*(1-Pb)+Pb*(1-Pa))。
这里计算区间[l1, r1]带有这个二进制位w的数字数量有个技巧。通常得到1~x的某种要求数量要比一个区间轻松一些,可以先计算1~r1再减去1~(l1-1)的会简单一些。
ll calc(ll n, ll w) //1到n二进制w出现次数
{
ll t = n / (w * 2) * w + max(0LL, n % (w * 2) - w + 1);
return t;
}
考察点:前缀和思想
官方题解的前缀和讲解博客
这道题的n达到了1e5,不能n2枚举数组中数字1两两计算距离进行求和,现在就要想个办法枚举一个1计算他到前面所有1的距离和。
拿这个长度为8的数组举例
下标:1 2 3 4 5 6 7 8
数组:0 1 0 1 1 0 1 0
我们枚举5号下标的1计算他前面所有1到他的距离和,如果我们暴力计算就是(5-1)+(5-4),现在优化他。
维护两个变量sum和cnt分别表示当前枚举的位置前面所有1的下标和与数量,当枚举到5时sum=1+4, cnt=2此时可以直接通过5*cnt-sum得到前面所有1到他的距离和。
考察点:线段树/树状数组、贡献
在F的基础上增加了两个操作:增加1或者删除1。
首先我们按照F题的方式来计算一遍初始答案,对于每次操作维护这个答案。如果操作的位置是p则这个位置与他前面的所有的1组成的贡献为p*cnt-sum,cnt为前面1的个数sum为前面1的下标和(同F题),与后面1的贡献为sum-p*cnt。如果添加1则加上共享删除则减去。
1 1 0 1 1
a b c d e
如果把c改为1答案增大:(c - b + c - a) + (d - c + e - c)
1 1 1 1 1
a b c d e
如果把c改为0答案减少:(c - b + c - a) + (d - c + e - c)
实现这个功能需要动态维护cnt和sum这两个"变量",此时需要线段树或树状数组这种数据结构来实现,附一树状数组模板:
struct BitTree //树状数组维护区间和 支持单点加 区间求和
{
ll c[N];
void Add(int x, ll v) //在x位置加v
{
while (x < N)
c[x] += v, x += lowbit(x);
}
ll Ask(int x)
{
ll t = 0;
while (x)
t += c[x], x -= lowbit(x);
return t;
}
ll Ask(int l, int r) //查询区间[l, r]的和
{
if (l > r)
return 0;
return Ask(r) - Ask(l - 1);
}
}cnt, sum;
树状数组详解
线段树详解
考察点:素数筛
因为n是固定的,可以先求出1到n每个数是否为质数,最后统计一下每个数的合数因子的个数,对于每个询问直接输出答案。
暴力找因子或单个根号n的算法过于缓慢,本题n为1e5推荐并使用埃式筛算法。
const int N = 1e5 + 100;
bool isp[N]; //是否为质数 若为质数则存false
int cnt[N], ans[N]; //合数因子个数 答案
for (int i = 2; i <= n; ++i) //埃式筛算法
if (!isp[i])
for (int j = i + i; j <= n; j += i)
isp[j] = 1;
else //附加内容 合数直接处理
{
for (int j = i; j <= n; j += i)
++cnt[j];
}
for (int i = 2; i <= n; ++i) //统计答案
++ans[cnt[i]];
四种素数筛法:朴素素数筛,埃氏筛,欧拉筛和区间筛
查考点:记忆化搜索
这个题递推版的动态规划也可以解决,不过记忆化搜索更加清晰(也算是动态规划)。下面对于这道题我们来讲一下怎么把一个会超时的暴力搜索改成记忆化搜索。
我们先把题目上给的那段递归求解汉诺塔的代码抄一下并稍加修改。
#include
using namespace std;
typedef long long ll;
ll f[10]; //答案 为了不写一堆if这里进行编码存在一个数组中
int code(int x, int y) //将x -> y的编码
{
return x * 3 + y;
}
void Hanoi(int n, int a, int b, int c)
{
if (n == 1)
++f[code(a, c)]; //不再输出 改为增加答案
else
{
Hanoi(n - 1, a, c, b);
++f[code(a, c)];
Hanoi(n - 1, b, a, c);
}
}
int main()
{
int n;
cin >> n;
Hanoi(n, 0, 1, 2); //初始三个柱子用012代表
ll sum = 0;
for (int i = 0; i < 10; ++i)
sum += f[i];
printf("A->B:%lld\n", f[1]);
printf("A->C:%lld\n", f[2]);
printf("B->A:%lld\n", f[3]);
printf("B->C:%lld\n", f[5]);
printf("C->A:%lld\n", f[6]);
printf("C->B:%lld\n", f[7]);
printf("SUM:%lld\n", sum);
return 0;
}
上面这段代码就是题目上给出的暴力搜索代码被我改为了C++版本,他的复杂度非常高达到了O(2n),这个复杂度对于n=60的题目是无法接受的。
现在我们考虑一个问题,我们多次调用Hanoi这个函数并传入相同的n、a、b、c那么得到的结果一定相同,而且在这个递归调用中绝大多数都和之前传入的参数相同,这时候我们不妨使用数组记录一下这个结果在相同时不再递归调用而是直接使用记录。
#include
using namespace std;
typedef long long ll;
ll f[70][4][4][4][10];
//不要被这个5维数组吓到 因为要记录n,a,b,c对应的大小为10的数组 所以根据数值范围再开4维就好了
int code(int x, int y) //将x -> y的编码
{
return x * 3 + y;
}
void Hanoi(int n, int a, int b, int c)
{
if (f[n][a][b][c][0] != -1) //如果这个参数nabc被处理过则0一定不等于-1 直接返回不再计算
return;
memset(f[n][a][b][c], 0, sizeof(f[n][a][b][c])); //接下来我们要计算 为-1会影响答案 清空
if (n == 1)
++f[n][a][b][c][code(a, c)]; //加的时候带上参数就好了
else
{
Hanoi(n - 1, a, c, b);
for (int i = 0; i < 10; ++i) //因为把答案都保存在最后的维度里面了 没有合并到一起现在来合并
f[n][a][b][c][i] += f[n - 1][a][c][b][i]; //加上递归的参数
++f[n][a][b][c][code(a, c)];
Hanoi(n - 1, b, a, c);
for (int i = 0; i < 10; ++i)
f[n][a][b][c][i] += f[n - 1][b][a][c][i];
}
}
int main()
{
memset(f, -1, sizeof(f)); //如果这个记录还没有被处理则被标记为-1
int n;
cin >> n;
Hanoi(n, 0, 1, 2);
ll sum = 0;
for (int i = 0; i < 10; ++i)
sum += f[n][0][1][2][i];
printf("A->B:%lld\n", f[n][0][1][2][1]);
printf("A->C:%lld\n", f[n][0][1][2][2]);
printf("B->A:%lld\n", f[n][0][1][2][3]);
printf("B->C:%lld\n", f[n][0][1][2][5]);
printf("C->A:%lld\n", f[n][0][1][2][6]);
printf("C->B:%lld\n", f[n][0][1][2][7]);
printf("SUM:%lld\n", sum);
return 0;
}
此时复杂度降低为O(n*35)。
聊聊动态规划与记忆化搜索
考察点:动态规划
题目要求我们按照一定的顺序与条件,走过某些点并获得他们的战斗力。
现在有3个怪物A,B,C,出现的时间是TA,TB,TC,假设按照 A-B-C 这个顺序走,会获得最大战斗力,首先最明显的应该满足的条件是TC>TB>TA,因为先去拿时间大的就拿不了时间小的怪物,首先按照时间排序,现在TA<TB<TC。
下面需要用上动态规划的思想最长上升子序列相关博客
设D[i]为走到从1到i所能获得的最大战斗力,对于怪物B,我们枚举从哪个怪物过来B,假如是A,他们需要满足条件dis(a,b)<=TB-TA;他们的时间差要大于等于两点间的距离才能从A开始并获得B。
枚举所有满足条件的A,来更新D[B]的答案,但是需要O(k^2)的复杂度,时限不够,但是实际上点只有200个,枚举的k个点,实际上都属于这200个点,我们每次只考虑处理过的时间点,对于怪物B,枚举N个点,每个点里都存储着处理过的怪物。我们需要的怪物是dis(a,b)<=TB-TA。即N个点内:TA<=TB-dis(a,b)的点的最大值,也就是区间[1,X]的最大值,这个X可以用二分法得到。
还需要维护N个点内前缀最值,再用数据结构显得繁琐甚至超时或者空间浪费,仔细观察可以发现,因为我们是按时间遍历的,所有每次处理过的怪物的时间都是递增的,那就好办了直接放进去的时候,统计前缀最值就好了。
维护两个数组time[maxn][maxn],MAX[maxn][maxn];
VAL[x] ={1,2,1,3,1}(这个是处理过的当前节点存储的最优答案)
time[X]={1,2,3,4,5}
MAX[x] ={1,2,2,3,3}
数组开不下,但是总量只有1e5,所以用vector存储。