暑假第二周笔记
sunset_breeze · · 个人记录
Week2笔记
本周学习了两个知识点:分块思想+莫队与矩阵乘法
Part 1分块思想
1.分块的概念
何为分块?个人对于它的理解就是加了稍微优化的暴力。那这种思想能干啥呢?它是基于根号思想维护"区间"之类的问题。但我们回顾"区间"问题,我们学过了暴力、树状数组、线段树等众多算法。比如给定一个长度为
虽然暴力法只能解决小规模的问题,但是它的代码非常简单。
有一种代码比树状数组和线段树简单,效率比暴力法高的算法,称为"分块"
,它能以
2.分块的操作
分块的基本操作如下
- 块的大小用
block 表示 - 块的数量用
t 表示 - 块的左右边界。定义数组
st[] 和ed[] ,用st[i] 和ed[i] 表示块i 的第i 个和最后一个元素的位置,有st[1]=1,ed[1]=block;st[2]=block+1,ed[2]=2\times block;...;st[i]=(i-1) \times block+1,ed[i]=i\times block... - 每个元素所属的块。定义
pos[],pos[i] 表示第i 个元素所在的块,pos[i]=(i-1)\div block+1
可以结合这张图进行理解
分块的实现
具体看下面的代码,其中
/*
分块
*/
int block=sqrt(n);
int t=n/block;
if(n%block)t++;
for(int i=1;i<=t;i++){
st[i]=(i-1)*block+1;
ed[i]=i*block;
}
ed[t]=n;
for(int i=1;i<=n;i++)
pos[i]=(i-1)/block+1;
for(int i=1;i<=t;i++){
for(int j=st[i];j<=ed[i];j++)
sum[i]+=a[j];
}
void change(int L,int R,int d){
int p=pos[L],q=pos[R];
if(p==q){
for(int i=L;i<=R;i++)a[i]+=d;
sum[p]+=d*(R-L+1);
}
else{
for(int i=p+1;i<=q-1;i++)add[i]+=d;
for(int i=L;i<=ed[p];i++)a[i]+=d;
sum[p]+=d*(ed[p]-L+1);
for(int i=st[q];i<=R;i++)a[i]+=d;
sum[q]+=d*(R-st[q]+1);
}
}
long long ask(int L,int R){
int p=pos[L],q=pos[R];
long long ans=0;
if(p==q){
for(int i=L;i<=R;i++)ans+=a[i];
ans+=add[p]*(R-L+1);
}
else{
for(int i=p+1;i<=q-1;i++)ans+=sum[i]+add[i]*(ed[i]-st[i]+1);
for(int i=L;i<=ed[p];i++)ans+=a[i];
ans+=add[p]*(ed[p]-L+1);
for(int i=st[q];i<=R;i++)ans+=a[i];
ans+=add[q]*(R-st[q]+1);
}
return ans;
}
4.块长与时间效率:
修改查询操作的时间复杂度都是
每次操作的时间复杂度为
5.分块的感想
分块算法代码简单粗暴,没有复杂数据结构和复杂逻辑,很容易编码。它的思想可以概括为 "整体打包维护,碎片逐个枚举"
Part 2莫队
1.普通莫队
形式
假设
解释
离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。
排序方法
对于区间
实现
void move(int pos, int sign) {
// update nowAns
}
void solve() {
BLOCK_SIZE = int(ceil(pow(n, 0.5)));
sort(querys, querys + m);
for (int i = 0; i < m; ++i) {
const query &q = querys[i];
while (l > q.l) move(--l, 1);
while (r < q.r) move(r++, 1);
while (l < q.l) move(l++, -1);
while (r > q.r) move(--r, -1);
ans[q.id] = nowAns;
}
}
2.带修莫队
特点
普通莫队是不能带修改的。
我们可以强行让它可以修改,就像 DP 一样,可以强行加上一维 时间维, 表示这次操作的时间。
时间维表示经历的修改次数。
即把询问
那么我们的坐标也可以在时间维上移动,即
-
[l-1,r,\text{time}] -
[l+1,r,\text{time}] -
[l,r-1,\text{time}] -
[l,r+1,\text{time}] -
[l,r,\text{time}-1] -
[l,r,\text{time}+1]
这样的转移也是
可以用和普通莫队类似的方法排序转移,做到
这一次我们排序的方式是以
过程
先考虑普通莫队的做法:
- 每次扩大区间时,每加入一个数字,则统计它已经出现的次数,如果加入前这种数字出现次数为
0 ,则说明这是一种新的数字,答案+1 。然后这种数字的出现次数+1 。 - 每次减小区间时,每删除一个数字,则统计它删除后的出现次数,如果删除后这种数字出现次数为
0 ,则说明这种数字已经从当前的区间内删光了,也就是当前区间减少了一种颜色,答案-1 。然后这种数字的出现次数-1 。
现在再来考虑修改:
- 单点修改,把某一位的数字修改掉。假如我们是从一个经历修改次数为
i 的询问转移到一个经历修改次数为j 的询问上,且i<j 的话,我们就需要把第i+1 个到第j 个修改强行加上。 - 假如
j<i 的话,则需要把第i 个到第j+1 个修改强行还原。
怎么强行加上一个修改呢?假设一个修改是修改第
- 加上这个修改:我们首先判断
pos 是否在区间[l,r] 内。如果是的话,我们等于是从区间中删掉颜色a ,加上颜色b ,并且当前颜色序列的第pos 项的颜色改成b 。如果不在区间[l,r] 内的话,我们就直接修改当前颜色序列的第pos 项为b 。 - 还原这个修改:等于加上一个修改第
pos 项、把颜色b 改成颜色a 的修改。
实现
#include <bits/stdc++.h>
#define SZ (10005)
using namespace std;
template <typename _Tp>
void IN(_Tp& dig) {
char c;
dig = 0;
while (c = getchar(), !isdigit(c))
;
while (isdigit(c)) dig = dig * 10 + c - '0', c = getchar();
}
int n, m, sqn, c[SZ], ct[SZ], c1, c2, mem[SZ][3], ans, tot[1000005], nal[SZ];
struct query {
int l, r, i, c;
bool operator<(const query another) const {
if (l / sqn == another.l / sqn) {
if (r / sqn == another.r / sqn) return i < another.i;
return r < another.r;
}
return l < another.l;
}
} Q[SZ];
void add(int a) {
if (!tot[a]) ans++;
tot[a]++;
}
void del(int a) {
tot[a]--;
if (!tot[a]) ans--;
}
char opt[10];
int main() {
IN(n), IN(m), sqn = pow(n, (double)2 / (double)3);
for (int i = 1; i <= n; i++) IN(c[i]), ct[i] = c[i];
for (int i = 1, a, b; i <= m; i++)
if (scanf("%s", opt), IN(a), IN(b), opt[0] == 'Q')
Q[c1].l = a, Q[c1].r = b, Q[c1].i = c1, Q[c1].c = c2, c1++;
else
mem[c2][0] = a, mem[c2][1] = ct[a], mem[c2][2] = ct[a] = b, c2++;
sort(Q, Q + c1), add(c[1]);
int l = 1, r = 1, lst = 0;
for (int i = 0; i < c1; i++) {
for (; lst < Q[i].c; lst++) {
if (l <= mem[lst][0] && mem[lst][0] <= r)
del(mem[lst][1]), add(mem[lst][2]);
c[mem[lst][0]] = mem[lst][2];
}
for (; lst > Q[i].c; lst--) {
if (l <= mem[lst - 1][0] && mem[lst - 1][0] <= r)
del(mem[lst - 1][2]), add(mem[lst - 1][1]);
c[mem[lst - 1][0]] = mem[lst - 1][1];
}
for (++r; r <= Q[i].r; r++) add(c[r]);
for (--r; r > Q[i].r; r--) del(c[r]);
for (--l; l >= Q[i].l; l--) add(c[l]);
for (++l; l < Q[i].l; l++) del(c[l]);
nal[Q[i].i] = ans;
}
for (int i = 0; i < c1; i++) printf("%d\n", nal[i]);
return 0;
}
Part3 矩阵乘法
矩阵乘法
矩阵的乘法是向量内积的推广。
矩阵相乘只有在第一个矩阵的列数和第二个矩阵的行数相同时才有意义。
设
其中矩阵
在矩阵乘法中,结果
线性代数研究的向量多为列向量,根据这样的对矩阵乘法的定义方法,经常研究对列向量左乘一个矩阵的左乘运算,同时也可以在这里看出「打包处理」的思想,同时处理很多个向量内积。
矩阵乘法满足结合律,不满足一般的交换律。
利用结合律,矩阵乘法可以利用 快速幂 的思想来优化。
在比赛中,由于线性递推式可以表示成矩阵乘法的形式,也通常用矩阵快速幂来求线性递推数列的某一项。
优化
首先对于比较小的矩阵,可以考虑直接手动展开循环以减小常数。
可以重新排列循环以提高空间局部性,这样的优化不会改变矩阵乘法的时间复杂度,但是会在得到常数级别的提升。矩阵加速递推
应用
斐波那契数列大家应该都非常的熟悉了。在斐波那契数列当中,
如果有一道题目让你求斐波那契数列第
设
试推导一个矩阵
怎么推呢?因为
综上所述:
转化为代码,应该怎么求呢?
定义初始矩阵
注意,矩阵乘法不满足交换律,所以一定不能写成
为什么要乘上
下面是求斐波那契数列第
const int mod = 1000000007;
struct Matrix {
int a[3][3];
Matrix() { memset(a, 0, sizeof a); }
Matrix operator*(const Matrix &b) const {
Matrix res;
for (int i = 1; i <= 2; ++i)
for (int j = 1; j <= 2; ++j)
for (int k = 1; k <= 2; ++k)
res.a[i][j] = (res.a[i][j] + a[i][k] * b.a[k][j]) % mod;
return res;
}
} ans, base;
void init() {
base.a[1][1] = base.a[1][2] = base.a[2][1] = 1;
ans.a[1][1] = ans.a[1][2] = 1;
}
void qpow(int b) {
while (b) {
if (b & 1) ans = ans * base;
base = base * base;
b >>= 1;
}
}
int main() {
int n = read();
if (n <= 2) return puts("1"), 0;
init();
qpow(n - 2);
println(ans.a[1][1] % mod);
}
这是一个稍微复杂一些的例子。
我们发现,
但是发现如果矩阵仅有这三个元素
于是考虑构造一个更大的矩阵。
我们希望构造一个递推矩阵可以转移到
转移矩阵即为