感谢大家的点赞,关注,评论。准备蓝桥杯的同学可以关注一下本专栏哦,不定期更新蓝桥杯笔记以及经验分享。本人多次参加过蓝桥杯,并获得过蓝桥杯国二的成绩。
算法模版笔记(Java)
这篇文章给大家分享我的蓝桥杯算法模版(Java版本)
快读
下面这个板子才是真正有用的(在蓝桥杯有用)
- 读入数字:用StreamTokenizer,先nextToken,再st.nval拿到值
- 读字符串:用BufferedReader,用readLine读。
- 如果数字和字符混读,那么在每一个数字读完再读String的时候要多用一个nextLine读入回车符
class Read{
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
public int nextInt() throws Exception{
st.nextToken();
return (int) st.nval;
}
public double nextDouble() throws Exception{
st.nextToken();
return st.nval;
}
public long nextLong() throws Exception{
st.nextToken();
return (long)st.nval;
}
public String nextLine() throws Exception{
return reader.readLine();
}
}
在蓝桥杯中,1e5数据量的输入需要用快读才能拿下更多的分
快写
println很慢,执行1e5次println需要1秒,执行1e6次print需要1秒,所以当输出超过1e6就要用快写
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
//......
out.flush();
邻接表
static int N = 110000 , M = 2*N;
static int[] h = new int[N],e = new int[M],ne = new int[M],w = new int[M];
static void add(int a,int b,int c) {
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
static void init() {
Arrays.fill(h, -1);
}
取模
处理负数的模是负数的问题,可以让负数的模是正数
static int getMod(int a,int mod) {
return (a%mod + mod)%mod;
}
快速幂
static long qpow(long a,long b) {
long res = 1;
while(b>0) {
if((b&1)==1) res = res*a%Mod;
b>>=1;
a = a*a%Mod;
}
return res;
}
//ps:用long来存,里面有乘法,会爆int的,即使题目里面的输入是int的。
模拟
日期
static int[] d = {0,31,30,31,30,31,30,31,31,30,31,30,31};
static int nextDay(int n) {
int year = n/10000;
int mon = n%10000/100;
int day = n%100;
//判断闰年,确定2月有多少天
if(year%4==0 && year%100!=0 || year%400==0)
d[2] = 29;
else d[2] = 28;
//计算
day++;
if(day>d[mon]) {
mon++;
day = 1;
}
if(mon>12) {
mon = 1;
year++;
}
return year*10000+mon*100+day;
}
//从开始日期枚举到结束日期
for(int i=from;i<=to;i=nextDay(i)) {
if(check(i))
ans++;
}
数据结构
前缀和
前缀和和差分数组都是从下标1开始
//初始化
sum[i] = sum[i-1] + arr[i];
//求和arr的[a,b]的和
sum[b] - sum[a-1];
二维前缀和
//1、初始化
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) {
s[i][j] = sc.nextInt();
s[i][j] += s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
//2.求和:求(x1,y1)和(x2,y2)中间的子矩形的和
System.out.println(s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]);
差分
前缀和的逆运算
作用:能够以O(1)的时间修改一个区间的值
原数组sum,差分数组chafen
//初始化
for(int i = 1;i<=n;i++) {
arr[i] = sc.nextInt();
chafen[i] = arr[i] - arr[i-1];
}
//操作一:对于原数组sum中的[l,r]中的值+v(这是离线操作,实际上sum没有被更新)
static void add(int l,int r,int v){
chafen[l]+=v;
chafen[r+1]-=v;
}
//操作二:计算最终结果(数组的最终值)
for(int i=1;i<=n;i++)
sum[i] = sum[i-1] + chafen[i];
//差分原理
chafen[i] = sum[i] - sum[i-1]
//最终数组的原理(修改结束后)
sum[i] = chafen[1] + chafen[2] + ... + chafen[i]
- 思考
- 差分数组第一个元素是a,后面元素全0:代表原数组的值都是a
- 特殊的,差分数组中元素全0:代表原数组全0
- chafen[i]会对sum[i+3]的值影响,要想把sum[i+3]降到0,就要先把chafen[i]降成0
二维差分
原理:和二维前缀和一样的公式
1.初始化:
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) {
s[i][j] = sc.nextInt();
chafen[i][j] = s[i][j] - s[i-1][j] - s[i][j-1] + s[i-1][j-1];
}
2.在(x1,y1)和(x2,y2)中间的子矩形中的所有元素都加c
chafen[x1][y1] += c;
chafen[x1][y2+1] -= c;
chafen[x2+1][y1] -= c;
chafen[x2+1][y2+1] += c;
3、计算最终数组
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + chafen[i][j];
System.out.print(s[i][j]+" ");
}
System.out.println();
}
离散化
-
如果一个数组,他的值域的范围很大,比如上界减下界的值大于1e8,但是数组的长度只有1e5,通常需要使用离散化,这类题目需要用把数组转化成只保留相对关系的离散化数组。
-
比如,arr = {33,88,44,111},可以转换成:w = {1,3,2,4};
-
方法就是对arr排序,并保留下标,最后把下标位置上的数换成排名.
-
步骤
- 1.排序
- 2.去重
- 3.二分寻找
-
严谨模版
//计算离散化数组O(nlogn) static Integer[] solve(int[] t,int sz) { TreeSet<Integer> se = new TreeSet<Integer>(); for(int i=1;i<=sz;i++) se.add(t[i]); Integer[] order = se.toArray(new Integer[0]); return order; } //二分找离散后的值 static int find(int x){ int l = 0,r = idx-1; while(l<r){ int mid = l+r+1>>1; if(order[mid]<=x) l = mid; else r = mid-1; } return l; } //二分,或者用arr[i]的时候查一下离散值 find(a[i]);
-
不严谨模版模板(适用于所有数都不同的情况)
//初始化结构体数组
Node[] arr = new Node[N];
for(int i=1;i<=n;i++){
arr[i] = new Node(nums[i],i);
}
//排序
Arrays.sort(arr,1,n+1,(a,b)->a.val-b.val);
//用排名代替值
for(int i=1;i<=n;i++){
w[arr[i].id] = i;
}
class Node{
int val,id;//id是下标位置
Node(int val,int id){
this.val = val;
this.id = id;
}
}
前缀和离散化
AcWing 802区间和:https://www.acwing.com/activity/content/code/content/6437889/
求区间和。
用treemap
-
本题就是前缀和,唯一的不同是数据范围,数组的下标是-1e9~1e9,不能用连续的数组去存储,需要离散化存储,java中用treemap作为离散化的前缀和数组,需要对下标排序,从而计算前缀和。
-
计算ketSet拿到所有的key,从而计算前一个位置的map值,
-
此外,所有的查询下标l,r也需要初始化在map中,否则就无法计算。这样map的最大值就要开到3e5,keys数组要这么大
-
查询的时候,需要返回l前一个key的值,而不是简单的l-1,所以需要用到treemap的api:lowerEntry(key),得到小于key的最大的key对应的实体
//输入
for(int i=1;i<=n;i++) {
int idx = sc.nextInt();
int val = sc.nextInt();
mp.put(idx,mp.getOrDefault(idx, 0)+val);
}
//查询的区间初始化到map中
for(int i=0;i<m;i++) {
int l = sc.nextInt();
int r = sc.nextInt();
q[i][0] = l;q[i][1] = r;
mp.put(l, mp.getOrDefault(l, 0));
mp.put(r, mp.getOrDefault(r, 0));
}
//拿到所有用到的下标
Integer[] keys = mp.keySet().toArray(new Integer[1]);
for(int i=1;i<keys.length;i++) {
mp.put(keys[i],mp.get(keys[i])+mp.get(keys[i-1]));//前缀和
}
for(int i=0;i<m;i++) {
int l = q[i][0];
int r = q[i][1];
if(l==keys[0])
// System.out.println(mp.ceilingEntry(r).getValue());//可以这么写
System.out.println(mp.get(r));
else
// System.out.println(mp.get(r)-mp.get(l-1));//不能这么写!!!
System.out.println(mp.get(r)-mp.lowerEntry(l).getValue());
}
字典树Tire
作用:高效的进行字符串存储和查询(也可以用来存二进制,参考题:最大异或对)
- 一棵26叉树,每个节点的二十六个孩子分别代表是否存在,存在就是>0的数,不存在就是0
- 根节点是0,如果son数组的值是0表示这个点不存在,每一个新的点都是 >= 1。
- N是所有字符串的长度和(s1.len + s2.len + …)的最大值
- 时间复杂度O(字符串的最大长度)
static int N = 101000;//所有字符串的长度和(s1.len + s2.len + ...)的最大值
static int[][] son = new int[N][26];
//son[i][k] = j表示:节点i的下一个位置存放的字符是k的节点索引是j
static int[] cnt = new int[N];
static int idx = 0;
//插入一个字符串
static void insert(char[] str) {
int p=0;//字典树指针,初始时指向根节点0
for(int i=0;i<str.length;i++) {
int u = str[i]-'a';
if(son[p][u]==0) //如果是0,就代表这个节点不存在,那么创建一个
son[p][u]= ++idx;
p = son[p][u];
}
cnt[p]++;
}
//查询字符串str出现的次数
static int query(char[] str) {
int p = 0;
for(int i=0;i<str.length;i++) {
int u = str[i]-'a';
if(son[p][u]==0) return 0;
p = son[p][u];
}
return cnt[p];
}
参考:https://www.acwing.com/activity/content/code/content/725149/
并查集
//存放父节点(祖先节点)
static int[] p = new int[N];
//找到x的祖先节点(路径压缩)
static int find(int x) {
if(x==p[x]) return x;
return p[x] = find(p[x]);
}
//初始化
for(int i=1;i<=n;i++)
p[i]=i;//每个节点都是根节点
//操作1:合并
p[find(a)]=find(b);
//操作2:查询
if(find(a)==find(b))//在同一个集合中
树状数组
两个操作(一维数组)
- 单点修改:修改某个位置上的数的值:O(logn)
- 区间求和:求一个区间前缀和:O(logn)
树状数组:
C[x] = sum(x-lowbit(x) , x] #左开右闭
普通前缀和虽然是O(1)的,但是一旦修改了某一个元素后,计算前缀和的时间复杂度就是O(n)了
普通前缀和是离线的,树状数组是在线的。(树状数组是根据下标的二进制后面有几个0进行划分层数的,比如3的二进制有0个0,那么就在最底层)
添加元素 求和
普通前缀和: O(1) O(n)
树状数组: O(logn) O(logn)
模板
- 树状数组下标从1开始的
//原数组:arr,树状数组:tree,=
static int lowbit(int x) {
return x&-x;
}
//在arr[idx]的值添加v
static void add(int idx,int v) {
//arr[idx] += v;//如果将idx变成v
for(int i=idx;i<=n;i+=lowbit(i))
tree[i]+=v;
}
//计算arr[1~x]的和
static int query(int idx) {
int res = 0;
for(int i=idx;i>0;i-=lowbit(i))
res += tree[i];
return res;
}
static int query(int l,int r){
return sum(r)-sum(l-1);
}
//初始化,如果初始是0不需要初始化
for(int i=1;i<=n;i++)
add(i,arr[i]);
//arr[idx]的值添加y
add(idx,y);
//计算[x,y]前缀和
query(x,y)
//arr[idx]修改成y
add(idx,y-arr[idx]);
权值树状数组
-
树状数组下标是值域(计算左边小于它的数的个数)
-
关键信息:树状数组的query(x)操作是计算一个前缀和,如果下标是数组中的元素值,那么刚好就可以logn得到个数。
-
思路:对于每个arr[i],每次需要求出左边/右边比它小的数的个数,由于数组长度的限制,而实际数组的值域很大,所以这种思路一般需要进行离散化处理,这样得到的数组的值域就是1<=y<=n,这样就能把值域作为树状数组的下标了。对于树状数组的每一次query,实际上是计算一个前缀和,比如query(x),返回值就是1~x中小于等于x的数的个数。
-
离散化数组w,保存的相对大小关系,w中存储的数是1~n,这个值域就是作为树状数组的下标
-
此时的tree[i]=j记录的是值为i的数的个数,每一次query(idx),实际就是找到小于等于idx的数的个数是j(而idx在这里正好是值)
思路参考:https://zhuanlan.zhihu.com/p/93795692
例题:lc315. 计算右侧小于当前元素的个数(树状数组+离散化)
应用
- lc315. 计算右侧小于当前元素的个数(树状数组+离散化)
- 计算逆序对的数量(可以用归并排序、也可以用树状数组):倒着求
- AcWing 241. 楼兰图腾
- AcWing 1215. 小朋友排队(结论+树状数组)
差分树状数组
树状数组的变形
-
区间修改、单点查询
-
思路:在树状数组上做差分
- 每一次修改[l,r]元素+d,相当于
add(l,d) ,add(r+1,-d)
- 每一次查询x处元素的值,相当于
sum(x)
- 差分树状数组的初始化
for(int i=1;i<=n;i++) { arr[i] = sc.nextInt(); add(i,(int)(arr[i]-arr[i-1])); }
- 每一次修改[l,r]元素+d,相当于
-
-
区间修改、区间查询
-
思路:推公式
-
树状数组维护前缀最大值
树状数组其实也能维护最大值,但是只能求前缀的最大值,不能求子区间的最大值(因为最大值没有减法)
//插入一个数,更新最大值
static void solveMax(int idx,long val) {
for(int i=idx;i<N;i+=lowbit(i))
tree[i]= Math.max(tree[i],val);
}
//计算1~idx中的最大值
static long maxPre(int idx) {
long res = 0;
for(int i=idx;i>0;i-=lowbit(i))
res = Math.max(res, tree[i]);
return res;
}
https://www.acwing.com/activity/content/code/content/6538977/
线段树
树状数组的扩展,应用场景远大于树状数组
- 单点修改、区间求和的板子如下
static int[] w = new int[N];//原数组
static Node[] tree = new Node[4*N];//线段树数组
//向上更新,根节点是u
static void pushup(int u) {
tree[u].val = tree[u<<1].val + tree[u<<1|1].val;
}
//构建线段树函数,根节点u,构建区间[l,r]
static void build(int u,int l,int r) {
//叶子节点直接记录权值
if(l==r)
tree[u] = new Node(l, r, w[l]);
else {
tree[u] = new Node(l, r, 0);//值先记为0,pushup再更新
int mid = l+r>>1;
build(u<<1, l, mid);//构建左子树
build(u<<1|1, mid+1, r);//构建右子树
pushup(u);//向上更新val
}
}
//计算当前节点u下的在[l,r]范围内的和,u代表当前节点(递归的时候l,r是不变的)
static int query(int u,int l,int r) {
//如果区间和val完全包含当前节点,就直接加上
if(tree[u].l>=l && tree[u].r<=r)
return tree[u].val;
//否则,当前节点的范围一分为二
int mid = tree[u].l + tree[u].r >>1;
int sum = 0;//这个参数根据具体题目变化,如果是求和这里就是sum
//如果左、右区间满足就加上
if(l <= mid) sum += query(u<<1,l,r);
if(r >= mid+1) sum += query(u<<1|1, l, r);
return sum;
}
//单点修改:给idx位置上的数+v。u代表当前节点
static void modify(int u,int idx,int v) {
//遍历到叶子节点就是具体的位置idx
if(tree[u].l==tree[u].r)
tree[u].val += v;
else {
//判断是在该节点u的左子树还是右子树上
int mid = tree[u].l + tree[u].r >>1;
if(idx<=mid) modify(u<<1, idx, v);
else modify(u<<1|1, idx, v);
pushup(u);//向上更新,这里非常容易忘记!!!
}
}
static class Node{
int l,r,val;
public Node(int l,int r,int val) {
this.l = l;
this.r = r;
this.val = val;
}
}
//初始化:
build(1,1,n);
//查询[a,b]的结果:
query(1,a,b);
//修改idx位置的值+v
modify(1,idx,v);
细节:
- 线段树数组tree是4倍N的区间(N是原数组最大长度)
- pushup是用子节点的信息更新父节点的信息
- build是初始化函数
- query是查询函数(最容易错),参数l和r在递归的时候都是不变的;mid是取树的左右边界的中点而不是l和r的中点;递归的出口是当前节点已经被完全包含就返回val,不是遍历到叶子节点。
- modify函数的mid也是计算树节点的中点,而不是l和r的中点;递归调用左右子树后一定要pushup
- 三个核心函数build,query,modify都是递归调用;只有quey有返回值;build、modify都需要pushup
带懒标记的线段树
-
应用场景:区间修改问题
-
描述:如果当修改的不是一个点而是一个区间的时候,使用懒标记能够依然以O(logn)的时间进行。
-
思路:修改[l,r]区间内的数,每次递归遍历到一个节点区间[a,b],当这个节点的区间[a,b]被[l,r]完全覆盖的时候,为了节约性能,我们直接在这个节点上加上懒标记(相当于记上这笔账),并更新val,然后直接返回。如果后面在进行修改/查询的时候,先进行PushDown,把父节点的账给孩子们算清楚,再去对孩子们进行操作
-
核心操作:
//向下更新:用父节点的状态更新子节点,把懒标记给儿子节点 //把父亲的账给儿子算清 static void pushDown(int u) { //传递懒标记 tree[u<<1].add += tree[u].add; tree[u<<1|1].add += tree[u].add; //传递最大值 tree[u<<1].val += tree[u].add; tree[u<<1|1].val += tree[u].add; tree[u].add = 0;//清空父节点的标记 } //区间修改:将[l,r]的值都+val static void modify(int u,int l,int r,int val) { //如果这个节点的区间被完全覆盖,就加上懒标记 if(tree[u].l>=l && tree[u].r<=r) { tree[u].add += val; tree[u].val += val; }else {//否则,不被完全覆盖,先把账算清再修改区间,最后更新到父节点 pushDown(u); int mid = tree[u].l+tree[u].r>>1; if(mid>=l) modify(u<<1,l,r,val); if(mid+1<=r) modify(u<<1|1,l,r,val); pushUp(u); } } //区间求和 static long query(int u,int l,int r) { if(tree[u].l>=l && tree[u].r<=r) return tree[u].val; //先算清账,再求内部的值 pushDown(u); long mx = INF; int mid = tree[u].l+tree[u].r>>1; if(l<=mid) mx = Math.min(mx,query(u<<1,l,r)); if(mid+1<=r) mx = Math.min(mx,query(u<<1|1,l,r)); return mx; }
lazy线段树模版维护:最大值
完整代码:
class Node{
int l,r;
long val;
long add;//懒标记
public Node(int l,int r,long val,long add) {
this.l = l;this.r = r;this.val = val;this.add = add;
}
}
static int[] w = new int[N];
static Node[] tree = new Node[4*N];
//向上更新:用子节点的值更新父节点
static void pushUp(int u) {
tree[u].val = Math.min(tree[u<<1].val,tree[u<<1|1].val);
}
//向下更新:用父节点的状态更新子节点,把懒标记给儿子节点
//把父亲的账给儿子算清
static void pushDown(int u) {
//传递懒标记
tree[u<<1].add += tree[u].add;
tree[u<<1|1].add += tree[u].add;
//传递最大值
tree[u<<1].val += tree[u].add;
tree[u<<1|1].val += tree[u].add;
tree[u].add = 0;//清空父节点的标记
}
static void build(int u,int l,int r) {
if(l==r) {
tree[u] = new Node(l,r,w[l],0);
}else {
tree[u] = new Node(l, r, 0, 0);
int mid = l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushUp(u);
}
}
//区间修改:将[l,r]的值都+val
static void modify(int u,int l,int r,int val) {
//如果这个节点的区间被完全覆盖,就加上懒标记
if(tree[u].l>=l && tree[u].r<=r) {
tree[u].add += val;
tree[u].val += val;
}else {//否则,不被完全覆盖,先把账算清再修改区间,最后更新到父节点
pushDown(u);
int mid = tree[u].l+tree[u].r>>1;
if(mid>=l) modify(u<<1,l,r,val);
if(mid+1<=r) modify(u<<1|1,l,r,val);
pushUp(u);
}
}
//区间求和
static long query(int u,int l,int r) {
if(tree[u].l>=l && tree[u].r<=r)
return tree[u].val;
//先算清账,再求内部的值
pushDown(u);
long mx = INF;
int mid = tree[u].l+tree[u].r>>1;
if(l<=mid) mx = Math.min(mx,query(u<<1,l,r));
if(mid+1<=r) mx = Math.min(mx,query(u<<1|1,l,r));
return mx;
}
//初始化
build(1,1,n);
//修改[l,r]内的每个数+d
modify(1, l, r,d);
//查询[l,r]
query(1, l, n)
lazy线段树维护:区间和
class SegTree_lz{
static int N = 101000;
static Node[] tree = new Node[4*N];
//向上更新:用子节点的值更新父节点
static void pushUp(int u) {
tree[u].val = tree[u<<1].val + tree[u<<1|1].val;
}
//向下更新:用父节点的状态更新子节点,把懒标记给儿子节点
//把父亲的账给儿子算清
static void pushDown(int u) {
//传递懒标记
tree[u<<1].add += tree[u].add;
tree[u<<1|1].add += tree[u].add;
//传递
tree[u<<1].val += tree[u].add * (tree[u<<1].r-tree[u<<1].l+1);
tree[u<<1|1].val += tree[u].add * (tree[u<<1|1].r-tree[u<<1|1].l+1);
tree[u].add = 0;//清空父节点的标记
}
static void build(int u,int l,int r) {
if(l==r) {
tree[u] = new Node(l,r,0,0);
}else {
tree[u] = new Node(l,r,0,0);
int mid = l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushUp(u);
}
}
//区间修改:将[l,r]的值都+val
static void modify(int u,int l,int r,int val) {
//如果这个节点的区间被完全覆盖,就加上懒标记
if(tree[u].l>=l && tree[u].r<=r) {
tree[u].add += val;
tree[u].val += val*(tree[u].r-tree[u].l+1);
}else {//否则,不被完全覆盖,先把账算清再修改区间,最后更新到父节点
pushDown(u);
int mid = tree[u].l+tree[u].r>>1;
if(mid>=l) modify(u<<1,l,r,val);
if(mid+1<=r) modify(u<<1|1,l,r,val);
pushUp(u);
}
}
//区间求和
static long query(int u,int l,int r) {
if(tree[u].l>=l && tree[u].r<=r)
return tree[u].val;
//先算清账,再求内部的值
pushDown(u);
long res = 0;
int mid = tree[u].l+tree[u].r>>1;
if(l<=mid) res += query(u<<1,l,r);
if(mid+1<=r) res += query(u<<1|1,l,r);
return res;
}
static class Node{
int l,r;
long val;
long add;//懒标记
public Node(int l,int r,long val,long add) {
this.l = l;this.r = r;this.val = val;this.add = add;
}
}
}
KMP
判断模式串(p)是不是主串(s)的子串,返回主串中出现子串的下标
时间复杂度O(n)
static char[] p,s;//p是模式串,s是主串
static int[] ne = new int[N];//p的next数组,ne[i]=j表示相等的前后缀最大长度
//n是模式串的长度,m是主串的长度。
//1.计算模式串p的next数组(背过)
static void getNext() {
for(int i=2,j=0;i<=n;i++) {
while(j>0 && p[i]!=p[j+1])
j=ne[j];
if(p[i]==p[j+1])
j++;
ne[i] = j;
}
}
//2.kmp匹配
for(int i=1,j=0;i<=m;i++) {
while(j>0 && s[i]!=p[j+1])
j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n) {//匹配成功
out.print(i-n+1+" ");//下标从1开始
j = ne[j];//计算后面是否还有子串p
}
}
关键点:
- 模式串p,主串s。字符串下标都是从1开始。n是p的长度,m是s的长度。
- 指针i指向主串,指针j指向模式串,每一轮是将i与j+1的位置比较,如果不满足,j就回退到ne[j]
- 如果i和j+1位置相同,那么就j++
- 为什么是j+1和i的位置比较???因为一旦找到不符合的,我们就需要找前一位前面的子串的next数组
字符串哈希
应用场景:给定一个字符串,快速判断这个子串的两个子串是否相等,时间复杂度O(1)
正常用substring(),最坏会达到O(n)
- 前缀和思想
- 把一个字符串映射成一个数,hash[i]存放前缀字符串str[1~i]的hash值
- 初始化:hash[i] = hash[i-1]*P + str[i]
- 计算子串str[l~R]的哈希值 = hash[r] - hash[l-1]*pow(P,r-l+1).画图就能推出来
static char[] str = new char[N];
static int[] hash = new int[N],pow = new int[N];
static int P = 131;//经验值
//初始化
pow[0]=1;
hash[0]=0;
for(int i=1;i<=n;i++) {
hash[i] = hash[i-1]*P + str[i];
pow[i] = pow[i-1]*P;//预处理次方
}
//计算子串的哈希值
static int query(int l,int r) {
return hash[r] - hash[l-1]*pow[r-l+1];
}
数学
https://www.acwing.com/blog/content/681/
判断质数(试除法)
时间复杂度O(sqrt(n))
static int is_prime(int n){
if(n<2) return 0;
for (int i=2;i<=n/i;i++){
if(n%i==0) return 0;
}
return 1;
}
质因数分解
时间复杂度O(sqrt(n))
- 用treemap存储
- 枚举2~根号n
- 最多只有一个大于根号n的质因子k,并且这个大于根号n的质因子k的幂一定是1。(反证法:参考证明)
static Map<Integer,Integer> mp = new TreeMap<>();//TreeMap有序
static void divide(int n){
for(int i=2;i<=n/i;i++){
int cnt = 0;//java里不能mp[i]++,所以用变量cnt存储,再赋值
while(n%i==0){
n/=i;
cnt++;
}
mp.put(i,cnt);
}
if(n>1) mp.put(n,1);
}
//输出
for(Integer key:mp.keySet()){
int val = mp.get(key);
if(val>0)
System.out.println(key+" "+mp.get(key));
}
素数筛
求n以内的所有素数
1.埃氏筛法(适合求每一个素数)(推荐)
时间复杂度(O(nlogn))
- 枚举n次
- 将素数的所有倍数筛掉
static int[] st;
static int[] primes;
static int cnt;
void get_primes(int n){
for(int i=2;i<=n;i++){
if(st[i]==0){
primes[cnt++]=i;
for(int j=i+i;j<=n;j+=i)
st[j]=1;
}
}
//求素数个数
cnt
}
2.优化的埃氏筛法(不适合求每一个素数)
时间复杂度(O(nloglogn))
- 枚举根号n次
- 还是筛质数的倍数,但是是从i*i开始筛,因为比i * i小的数已经被前面的晒过了
void getPrimes(int n){
for(int i=2;i<=n/i;i++){//根号n次
if(st[i]==0){
for(int j=i*i;j<=n;j+=i)//i*i开始
st[j]=1;
}
}
//求素数个数
for(int i=2;i<=n;i++)
if(st[i]==0) ans++;
//求每一个素数
// for(int i=2;i<=n;i++)
// if(st[i]==0) primes[cnt++]=i;
}
3.线性筛(性能最好)
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
求约数
时间复杂度 sqrt(n)
static ArrayList<Integer> yue = new ArrayList<>();
static void solve(int n){
for (int i = 1; i <= n/i; i++) {
if(n%i==0){
yue.add(i);
if(n/i!=i)
yue.add(n/i);
}
}
Collections.sort(yue);//约数从小到达排
}
约数之和、约数个数
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
求组合数
求C(a,b)
用long存!!!
- N 在3000以内(题意不用取模)
static long[][] C = new long[N][N];
static void init(){
for (int i = 0; i < N; i++) {
for (int j = 0; j <= i; j++) {
if(j==0) C[i][j] = 1;
else C[i][j] = C[i-1][j] + C[i-1][j-1];
//对于要对答案取模的时候,在计算组合数的时候就要取模
//else C[i][j] = (C[i-1][j] + C[i-1][j-1])%Mod;
}
}
}
-
N在10^8以内、题目说要取模(用逆元求)
C(a,b) = a!/(b! * (a-b)!) = a! * niyuan(b!) * niyuan((a-b)!)
由于询问比较多,直接初始化阶乘和阶乘的逆元数组
递推式:
n! = (n-1)! * n
1/(n!) = 1/(n-1)! * 1/n
,其中1/n = qpow(n,Mod-2)
(费马小定理)
//jiechen[i] = i!
static long[] jiechen = new long[N];
//jiechenniyuan[i] = “i!的逆元”
static long[] jiechenniyuan = new long[N];
//初始化阶乘、阶乘的逆元
static void init() {
jiechen[0] = 1;
jiechenniyuan[0] = 1;
for(int i=1;i<N;i++) {
jiechen[i] = jiechen[i-1] * i%Mod;
jiechenniyuan[i] = jiechenniyuan[i-1] * qpow(i, Mod-2)%Mod;
}
}
//C(a,b) = a!/(b! * (a-b)!) = a! * niyuan(b!) * niyuan((a-b)!)
static long C(int a,int b) {
long res = 1;
res = res * jiechen[a]%Mod;
res = res * jiechenniyuan[b]%Mod;
res = res * jiechenniyuan[a-b]%Mod;
return res;
}
https://www.acwing.com/activity/content/code/content/5904493/
如果题目里面没有说要对组合数取模,那么这种方法不能使用!!!!!!!!
判断其他点是否在同一条直线上
已知两个点(x0,y0)(x1,y1)从而可以确定一条直线,再去判断一个点(x,y)是否在这条直线上面,有如下公式。用的是直线的一般形式。
由于除法会出问题,所以一般都是使用直线的一般形式: ( Y − y 0 ) / ( X − x 0 ) = ( y 1 − y 0 ) / ( x 1 − x 0 ) (Y-y0)/(X-x0) = (y1-y0)/(x1-x0) (Y−y0)/(X−x0)=(y1−y0)/(x1−x0),化简得 ( Y − y 0 ) ∗ ( x 1 − x 0 ) = = ( X − x 0 ) = ( y 1 − y 0 ) (Y-y0) * (x1-x0) == (X-x0) = (y1-y0) (Y−y0)∗(x1−x0)==(X−x0)=(y1−y0),我们只需要判断这个东西是不是相等即可判断是否在同一条直线上
- 例子:x0,y0固定,统计经过(x0,y0)的所有直线中,至少需要多少根直线能够把所有给定的点覆盖掉。
//这里x0,y0是固定的
for(int i=0;i<n;i++) {//枚举第二个点,确定这条直线
if(st[i]==1) continue;//这个点已经在其他的直线上了,就不需考虑这个点了
int x1 = arr[i].x;
int y1 = arr[i].y;
ans++;//这是一条新的直线,计数+1
for(int j=i+1;j<n;j++) {
int x = arr[j].x;
int y = arr[j].y;
//使用直线的一般公式判断点(x,y)是否在这条直线上
if((x-x0)*(y0-y1) == (y-y0)*(x0-x1))
st[j] = 1;
}
}
费马小定理
描述:
如果一个数p是质数,并且a不是p的倍数,那么有a^(p-1) = 1 (mod p)。
除以p同余
等价于:(推荐)
如果一个数p是质数,并且a和p互质(等价于gcd(a,p)=1),那么有a^(p-1) = 1 (mod p)
底层逻辑:a是p的倍数,那么gcd(a,p) = p;如果a不是p的倍数,那么gcd(a,p)不一定是1,但是当a和p有一个是质数时,那么此时gcd(a,p)=1。
互质的理解:gcd(a,p)=1。如果两个数都是质数,那么一定互质。如果两个数有一个是质数,另外一个数不是它的倍数,那么这两个数互质,原因:质数只有1和他本身两个约数,不是倍数,那么他本身不能成为公约数了,那么公约数只能是1了。
官方解释:
一般配合求逆元使用
逆元
定义
若整数b,m互质,并且对于任意的整数 a,如果满足b|a,则存在一个整数x,使得 a/b≡a∗x(mod m) ,同余,则称x为b的模m乘法逆元,记为b^{-1}(mod m)。
理解:
用逆元把除法变成了乘法。除法是一个很麻烦的事情,所以上述构造的含义:a除以一个数,我们要把他变成a乘以一个数,除以b,相当于乘以x,那么 b − 1 = x b^{-1} = x b−1=x。即 b*x == 1(mod m)
求逆元(充要条件):
b存在乘法逆元的充要条件是 b与模数 m互质。
1.当模数 m为质数时,b^(m−2)即为b的模m的乘法逆元。(利用费马小定理)
2.当模数 m不是质数时,就需要用扩展欧几里得算法求出来的x就是一个逆元
证明看:费马小定理+逆元证明
欧拉函数
1∼N 中与 N 互质的数的个数被称为欧拉函数,记为 ϕ(N)。
若在算数基本定理中,下图中的p是n的质因子
int solve(int n){
int res=n;
for(int i=2;i<=n/i;i++){
if(n%i==0){
res =res /i * (i-1);
while(n%i==0)n/=i;
}
}
if(n>1) res = res/n*(n-1);
return res;
}
扩展欧几里得算法
裴蜀定理
- 若a,b是整数,且gcd(a,b)=d,那么对于任意的整数x,y,都有ax+by都一定是d的倍数。(充要的)
特别地,一定存在整数x,y,使ax+by=d成立。
换句话说:
- 两个整数a,b,方程
a*x+b*y=m
有解,当且仅当m是gcd(a,b)的倍数(充要)
扩展欧几里得代码:
// 求x, y,使得ax + by = gcd(a, b)
// 扩展欧几里得:求解方程ax+by=gcd(a,b)的解
// x=y′ y=x′−[a/b]*y′
static int x, y;//全局变量,替代引用
int exgcd(int a,int b){
if(b==0){
x=1,y=0;
return a;
}
int gcd=exgcd(b,a%b);//递归调用
int tmp = x;
x=y,y=tmp-a/b*y;
return gcd;
}
//结果说明
1.exgcd()的返回值是最大公约数
2.最后的(x,y)是方程ax + by = gcd(a,b)的解
3.如果exgcd()的结果是1(那么a和b互质,就存在逆元),那么x是a的逆元(x可能是负数,所以答案是getMod(x))
参考:https://www.cnblogs.com/kksk/p/13069795.html
扩展欧几里得求逆元
板子已经给出来了,下面给出说明。
目标:求a的逆元x,a*x==1(% mod),a不是质数
我们假设逆元存在,已知逆元存在的充要条件是:gcd(a,mod) == 1
已知裴蜀定理:ax + by = gcd(a, b)的方程一定有解
另b = mod
则,a * x + mod * y = gcd(a,mod) = 1
两边模mod得: a*x % mod = 1,即 a * x = 1(%mod)
此时的x就是a的模mod的逆元,x也就是方程ax+by=gcd(a,b)的解的横坐标
1~n中k的倍数的个数
1~n中k的倍数的个数可以表示为:n/k
举例:1~8中2的倍数的个数为:8/2=4
n!中质因子p的个数
根据公式,n!中有质因子p的个数为:
(
n
p
+
n
p
2
+
n
p
3
+
.
.
.
)
(\frac {n}{p}+\frac {n}{p^2}+\frac {n}{p^3}+...)
(pn+p2n+p3n+...)
时间复杂度为:O(logn)
int divide(int n,int p){
int ans=0;
/*方案1
while(n){
ans+=n/p;
n/=p;//不能写成p*=p;
}
*/
//方案2
for(int i=p;n/i;i*=p)
ans+=n/i;
return ans;
}
参考:https://blog.csdn.net/Supreme7/article/details/115796373
搜索
BFS
static char[][] arr = new char[N][N];
static int[][] vis = new int[N][N];
static int[][] cen = new int[N][N];
static int t,r,c;
static int sx,sy,ex,ey;
static int[] dx = {1,0,-1,0};
static int[] dy = {0,1,0,-1};
static void bfs() {
for(int i=0;i<N;i++) {
Arrays.fill(cen[i], INF);
Arrays.fill(vis[i], 0);
}
Queue<PII> q = new LinkedList<>();
//起点入队
q.add(new PII(sx,sy));
cen[sx][sy] = 0;
vis[sx][sy] = 1;
while(q.size()>0) {
PII top = q.poll();
int x = top.x, y = top.y;
for(int i=0;i<4;i++) {
int nx = x + dx[i],ny = y + dy[i];
if(check(nx,ny)==1) {
q.add(new PII(nx,ny));
vis[nx][ny] = 1;
cen[nx][ny] = cen[x][y]+1;
ans++;//访问的不同节点个数
}
}
}
return cen[ex][ey];
}
class PII{
int x;
int y;
public PII(int x,int y) {
this.x = x;
this.y = y;
}
}
static int check(int x,int y) {
if(x>=1 && x<=r && y>=1 && y<=c && vis[x][y]==0 && arr[x][y]=='.')
return 1;
return 0;
}
public static void main(String[] sss) {
int ans = bfs();
if(ans!=INF)
System.out.println(ans);
else
System.out.println("oop!");
}
}
BFS遍历前k层所有节点的模板
就遍历k次,每一次初始时队列中元素的个数sz,让sz个
static int bfs(int u) {
Queue<Integer> q = new LinkedList<Integer>();
Arrays.fill(st, 0);
q.add(u);
st[u] = 1;
int ans = 0;//答案不算起点
for(int level = 0;level < k;level++) {//BFS最多k层
int sz = q.size();//获得当前这一层的所有节点个数
while(sz-->0) {//把当前这层的所有节点出队
Integer top = q.poll();
for(int i=h[top];i!=-1;i=ne[i]) {
int j = e[i];
if(st[j]==1) continue;
q.add(j);
st[j]=1;
ans++;
}
}
}
return ans;
}
枚举上下左右四个方向
static int[] dx = {1,0,-1,0};
static int[] dy = {0,1,0,-1};
for(int i=0;i<4;i++){
int nx = x + dx[i];
int ny = y + dy[i];
}
枚举上下左右的八个方向
for(int i=x-1;i<=x+1;i++){
for(int j=y-1;j<=y+1;j++) {
if(i==x && j==y) continue;//去除中间元素本身
//具体代码
}
}
FloodFill
枚举每个位置,如果是水域,并且没有被标记过,那么就搜索这一整片池塘,搜索的过程可以是bfs,也可以是dfs,对于搜索过的每一块水域,都标记一下。
https://www.acwing.com/activity/content/code/content/6361986/
图论
图的存储
邻接表
专栏下其他博客有
import java.util.*;
public class Main {
static int N = 6060;
static int[] h = new int[N],e = new int[N],ne = new int[N];//邻接表
static int idx;
static void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
}
//初始化头数组
Arrays.fill(h,-1);
//遍历root的所有子节点
for(int i = h[root];i!=-1;i = ne[i]){
int j = e[i];
//...
}
邻接表2
用来快速得到顶点的所有邻边条数
leetcode中比较常见
ArrayList<Integer>[] g = new ArrayList[N];
//初始化
for(int i=0;i<n;i++)
g[i] = new ArrayList<Integer>();
//顶点a,b中间添加一条边
g[a].add(b);
Dijkstra
O (mlogn)
static int[] h = new int[N],e = new int[M],ne = new int[M],w = new int[M];
static int idx;
static int[] dist = new int[N],st = new int[N];
static void add(int a,int b,int c) {
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
static int dijkstra() {
Arrays.fill(dist, INF);
PriorityQueue<PII> q = new PriorityQueue<PII>((a,b)->(a.dist-b.dist));//小根堆
q.add(new PII(0,1));
dist[1]=0;
while(q.size()>0) {
PII top = q.poll();
//如果这个点的最短路确定了,那么直接跳过
if(st[top.ver]==1) continue;
st[top.ver]=1;
for(int i=h[top.ver];i!=-1;i=ne[i]) {
int j = e[i];
if(dist[j]>dist[top.ver]+w[i]) {
dist[j] = dist[top.ver]+w[i];
q.add(new PII(dist[j],j));
}
}
}
return dist[n];
}
}
class PII{
int dist,ver;
public PII(int dist,int ver) {
this.dist = dist;
this.ver = ver;
}
}
SPFA最短路
时间复杂度O(m*n)但是实际效率很高,用来替代朴素版的dijkstra
//st数组用来存储当前节点是否在队列中
static int[] st = new int[N],dist = new int[N];
static int spfa() {
Arrays.fill(dist, INF);
Queue<Integer> q = new LinkedList<Integer>();
q.add(1);
st[1] = 1;
dist[1] = 0;
while(q.size()>0) {
Integer top = q.poll();
st[top] = 0;//不在队列了,置为0
for(int i=h[top];i!=-1;i=ne[i]) {
int j = e[i];
if(dist[j] > dist[top] + w[i]) {
dist[j] = dist[top] + w[i];
if(st[j]==0) {//不在队列中,就入队
st[j]=1;
q.add(j);
}
}
}
}
return dist[n];
}
Floyd
- floyd算法只需要一个二维数组变量g[][],运行前是一个邻接矩阵,运行后是一个记录最短路的dist数组,g [ i ] [ j ]表示i到j的最短路
- 这个算法用邻接矩阵存储(朴素dijkstra用邻接矩阵,堆优化用邻接表,spfa用邻接表,floyd用邻接矩阵)
- 初始化,对于所有的i!=j的位置,g数组初始化成INF。
static int floyd(int[][] g) {
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
g[i][j] = Math.min(g[i][j], g[i][k]+g[k][j]);
return g[1][n];
}
//初始化,在输入之前
for(int i=0;i<n;i++) {
Arrays.fill(g[i], INF);
tie[i][i] = gon[i][i] = 0; //自己到自己的距离为0
}
//最短距离
g[1][n];//表示从顶点1到顶点n的最短路径
Kruskal
static int[] p = new int[N];
static Edge[] edges = new Edge[M];
static int kruskal() {
Arrays.sort(edges,1,m+1);
int cnt = 0,ans = 0;
for(int i=1;i<=m;i++) {
int x = edges[i].x;
int y = edges[i].y;
int w = edges[i].w;
int a = find(x),b = find(y);
if(a!=b) {
p[a] = b;
cnt++;
ans += w;
}
}
return cnt==n-1?ans:INF;
}
class Edge implements Comparable<Edge>{
int x,y,w;
public Edge(int x,int y,int w) {
this.x = x;
this.y = y;
this.w = w;
}
@Override
public int compareTo(Edge o) {
return Integer.compare(w, o.w);
}
}
static int find(int x) {
if(x==p[x]) return x;
return p[x] = find(p[x]);
}
bellmanFord算法求最多k条边的最短路
应用场景:求边数最大是k的最短路
循环k次,每一次只用上一轮的dist数组更新,所以在循环边时把dist保存为tmp,防止本轮使用了这一轮的dist值
static Edge[] edges = new Edge[N];
static int[] dist = new int[N],tmp = new int[N];
static void bellmanFord() {
Arrays.fill(dist,INF);
dist[1]=0;
for(int i=0;i<k;i++) {//循环k次,代表
//复制当前的dist,表示上一轮的dist结果,这样才能找到边数<=k的
tmp = dist.clone();
for(int j=0;j<m;j++) {
Edge e = edges[j];
dist[e.b] = Math.min(dist[e.b], tmp[e.a]+e.c);
}
}
}
class Edge{
int a,b,c;
public Edge(int a,int b,int c) {
this.a = a;
this.b = b;
this.c = c;
}
}
拓扑排序
只有有向无环图才有拓扑序列,图不一定要连通
1.所有入度为0的结点入队
2.当队列(栈)不为空时循环
2.1取队头,出队,记录序列
2.2遍历队头顶点指向的顶点,使该顶点入度-1
2.3入度为0时入队
时间复杂度 O(V+E)
注意:
- cnt用来存储已经输出的顶点的个数
- 只有有向无环图才有拓扑序列
- 如果一个图有环,最后终止循环时的cnt一定是小于n的
- 所以只需判断cnt==n? 如果相等,就有拓扑序列,否则就不存在
int[] d = new int[N];//存放入度
int[] print = new int[N];//记录答案
int cnt;
static boolean topSort() {
Queue<Integer> q = new LinkedList<Integer>();
for(int i=1;i<=n;i++) {
if(d[i]==0)
q.add(i);
}
while(q.size()>0) {
Integer top = q.poll();
print[cnt++] = top;
for(int i=h[top];i!=-1;i=ne[i]) {
int j = e[i];
d[j]--;
if(d[j]==0) {
q.add(j);
}
}
}
return n==cnt;
}
染色法判定二分图
-
二分图概念:
- 二分图:图中点通过移动能分成左右两部分,左侧的点只和右侧的点相连,右侧的点只和左侧的点相连。
- 一个图是二分图的充要条件是:当且仅当图中不含有奇数环
- 奇数环:由奇数条边形成的一个环
-
应用:判断图是否是二分图
-
时间复杂度O(n)
-
染色法思路:
-
开始对任意一未染色的顶点染色。
-
判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色。
-
若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断。
-
bfs和dfs可以搞定!
static int[] color = new int[N];
//U是节点编号,c是节点u应该变成的颜色
static boolean dfs(int u,int c) {
color[u] = c;
for(int i=h[u];i!=-1;i=ne[i]) {
int j = e[i];
if(color[j]==0) {
//return dfs(j,c==1?2:1);//这么写会WA
boolean res = dfs(j,c==1?2:1);
if(res==false) return false;//只有当返回false的时候,才能得到:不是二分图的结论,如果返回true不能返回值,而要继续搜索
}else if(c==color[j])
return false;
}
return true;
}
int flag = 1;
for(int i=1;i<=n;i++) {
if(color[i]==0) {
boolean res = dfs(i,1);
if(res==false) {
flag = 0;
break;
}
}
}
if(flag==1) System.out.println("Yes");
else System.out.println("No");
匈牙利算法
二分图最大匹配问题
https://www.acwing.com/activity/content/code/content/6469504/
-
应用:匈牙利算法解决二分图的最大匹配问题
-
思路:
- 类比:一张桌左右两旁分别有n1个男生,n2个妹子。图中的边是暧昧的对象。然后看看最多能有多少对在一起。
- 枚举左边的所有节点,依次判断当前左边节点的所有右边暧昧对象,如果右边的暧昧对象没有对象,那么就直接在一起;否则如果暧昧对象已经有对象了,那么就找到这个妹子的对象,他在他的暧昧对象中重新去找,看看他能不能找到一个其他的暧昧对象,如果能够找到一个,那就让他们在一起,从而这个妹子就是我的了。
-
时间复杂度O(m*n),但是实际的执行效率高很多
-
模板
//match[i]=j表示右半边节点i当前匹配的是左半边的节点j
//st是临时预定数组,st[i]表示当前轮次的右半边节点i是否被标记过
static int[] match = new int[N],st = new int[N];
//find函数:左边节点u向右边匹配,返回值为是否能够匹配成功
//返回值:如果加入x来参与模拟配对,会不会使匹配数增多
static boolean find(int u) {
for(int i=h[u];i!=-1;i=ne[i]) {
int j = e[i];
if(st[j]==0) {//如果在这一轮模拟匹配中,这个女孩尚未被预定
st[j] = 1;//那u就预定这个女孩了
//如果女孩j没有男朋友,或者她原来的男朋友能够预定其它喜欢的女孩。配对成功,更新match
if(match[j]==0 || find(match[j])) {
match[j] = u;
return true;
}
}
}
//自己中意的全部都被预定了。配对失败。
return false;
}
//枚举所有男生,依次进行匹配
for(int i=1;i<=n1;i++) {
//因为每次模拟匹配的预定情况都是不一样的所以每轮模拟都要初始化
Arrays.fill(st,0);//每一轮都进行匹配
if(find(i)) ans++;
}
LCA(最近公共祖先)
-
depth[i] = j表示节点i的层数(深度)是j
-
fa[u][i]
表示u向上跳2i层的祖先节点,第二维开多大取决于节点个数最大值,比如节点有5e5,那么第二维就开到20,因为220 = 1e6,最大指数就是19 -
节点编号是从1开始,0会作为超出边界的哨兵
-
思路
-
1.预处理st表(倍增)
从小到大枚举,
fa[u][i] = fa[fa[u][i-1]][i-1]
-
2.lca查询(二进制拆分)从大到小枚举
- 1.把a,b跳到同一层
- 2.a,b同时向上跳
-
static int[] depth = new int[N];//存储节点的层数
static int[][] fa = new int[N][20];//最大2^19次方
static int n,m,start;
//st表预处理(计算倍增数组)O(nlogn)
static void dfs(int u,int father) {
depth[u] = depth[father]+1;
fa[u][0] = father;
for(int i=1;i<=19;i++) //递推
fa[u][i] = fa[fa[u][i-1]][i-1];//先跳2^i-1,再跳2^i-1
for(int i=h[u];i!=-1;i=ne[i]) {
int j = e[i];
if(j==father) continue;
dfs(j,u);
}
}
//lca求公共祖先O(logn)
static int lca(int a,int b) {
if(depth[a]<depth[b]) {
int t = a;
a = b;
b = t;
}
//1.把a和b移到同一层
for(int i=19;i>=0;i--)
//如果跳2^i层还是在b的下面,就跳到这个位置
if(depth[fa[a][i]]>=depth[b])
a = fa[a][i];
if(a==b) return b;
//2.a和b同时向上找到最近公共祖先的下一层节点
for(int i=19;i>=0;i--) {
//如果a和b跳上去不是同一个节点(这里的条件舍去了跳出边界的情况)
if(fa[a][i] != fa[b][i]) {
a = fa[a][i];
b = fa[b][i];
}
}
return fa[a][0];//再向上走1层就到了lca节点了
}
//主函数
dfs(start,0);//不能是-1,因为father要用到depth
m = sc.nextInt();
while(m-->0) {
int x = sc.nextInt();
int y = sc.nextInt();
int ans = lca(x,y);
if(ans==x) System.out.println(1);
else if(ans==y) System.out.println(2);
else System.out.println(0);
}
- 例题
- AcWing 1171. 距离(顶点距离)
- AcWing 4962. 景区导游(顶点距离)
树上差分
差分数组是可以用O(1)将区间内元素+c,最后只需O(n)遍历数组。树上差分用于高效解决树上的将区间内元素+c。
按点差分
定义树上差分数组p,p[i]+=c
表示将树中根节点向节点i的路径上的所有边的值都+c,所以怎么将树中两个节点a,b路径上的所有节点都+c呢,操作就是:
p[a]+=c;
p[b]+=c;
p[lca]-=c;
p[fa[lca][0]]-=c
其中lca表示a,b的最近公共祖先,fa[lca][0]
表示的是节点lca的父节点
拓展:树上顶点间的距离
距离公式:
顶点a到顶点b的距离 = dist[a] + dist[b] - 2*dist[lca]
新增一个数组dist,表示顶点到起点的距离
dist更新的方式如下:
static void dfs(int u,int father) {
dep[u] = dep[father]+1;
fa[u][0] = father;
for(int i=1;i<19;i++)
fa[u][i] = fa[fa[u][i-1]][i-1];
for(int i=h[u];i!=-1;i=ne[i]) {
int j = e[i];
if(j==father) continue;
dist[j] = dist[u] + w[i];
dfs(j,u);
}
}
按边差分
如果要维护边的差分数组,比如对树上a,b路径上的所有边都+c,操作就是:
p[a]+=c;
p[b]+=c;
p[lca(a,b)]-=2*c
这里是将边下放到节点,比如p[a] = b,表示顶点a为下边界时的那条边
最后从根节点向下遍历整张图中所有节点,先递归子节点,再将节点从下向上更新前缀和数组(差分的求和),板子如下
//先递后归
static void dfs(int u,int father) {
for(int i=h[u];i!=-1;i=ne[i]) {
int j = e[i];
if(j==father) continue;
dfs(j,u);
chafen[u] += chafen[j];
//如果要在这判断边,是使用chafen[j]这条边,因为这条边的值已经确定,而chafen[u]还没确定
}
}
需要注意的是,树上差分算法涉及到LCA,如果是用倍增求LCA,那么每一个求lca是O(logn)的复杂度;如果是用Tarjan求LCA,那么预处理后每一个求lca是O(1)的
- 例题
- AcWing 352. 闇の連鎖(边的树上差分)
- luogu P3258 松鼠的新家(顶点的树上差分)
排序算法
快速排序
时间复杂度O(nlogn)
递归树的高度是logn,每一层的元素和还是n,所以nlogn
static void quickSort(int[] arr,int l,int r) {
if(l>=r) return ;
int x = arr[l],i = l-1,j = r+1;
while(i<j) {
while(arr[++i] < x);
while(arr[--j] > x);
if(i<j) {
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
quickSort(arr, l, j);
quickSort(arr, j+1, r);
}
快排可以求第k大的数
归并排序
static int[] tmp = new int[N];//临时数组
static void mergeSort(int[] a,int l,int r) {
if(l>=r) return;
int mid = l + r>>1;
mergeSort(a, l, mid);
mergeSort(a, mid+1, r);
int k=0;//临时数组tmp的下标计数
int i=l,j=mid+1;
while(i<=mid && j<=r) {
if(a[i]<=a[j]) tmp[k++] = a[i++];
else tmp[k++] = a[j++];
}
while(i<=mid) tmp[k++] = a[i++];
while(j<=mid) tmp[k++] = a[j++];
for(i=l,j=0;i<=r && j<k;i++,j++) {
a[i] = tmp[j];
}
}
归并排序可以用来求逆序对的数量
逆序对的数量
分治的思想,用O(nlogn)的时间复杂度,求出一个数列中逆序对的个数,修改归并排序的代码
//返回区间内元素的逆序对的个数
static long getNixudui(int[] q,int l,int r) {
if(l>=r) return 0;
int mid = l+r>>1;
long res = getNixudui(q, l, mid) + getNixudui(q, mid+1, r);
int i = l, j = mid+1;
int k = 0;
while(i<=mid && j<=r) {
if(q[i]<=q[j]) tmp[k++] = q[i++];
else {
res += mid - i +1;//当前j的逆序对的个数
tmp[k++] = q[j++];
}
}
while(i<=mid) tmp[k++] = q[i++];
while(j<=r) tmp[k++] = q[j++];
for(i = l,j = 0;i<=r;i++,j++)
q[i] = tmp[j];
return res;
}
链接:https://www.acwing.com/activity/content/code/content/5530302/
也可以用树状数组
Node[] arr = new Node[N];
int[] lisanhua = new int[N];
//离散化
for(int i=1;i<=n;i++){
arr[i] = new Node(nums[i],i);
}
Arrays.sort(arr,1,n+1,(a,b)->a.val-b.val);
for(int i=1;i<=n;i++)
lisanhua[arr[i].id] = i;
//记录个数
int cnt = 0;
for(int i=n;i>0;i--){//从后向前枚举
cnt += query(lisanhua[i]-1);//找到小于lisanhua[i]的个数
// System.out.println(query(lisanhua[i]-1));
add(lisanhua[i],1);
}
return cnt;
堆
堆是二叉树的结构,我们要利用堆实现以下五个操作:
- 插入一个数
- 求集合最值
- 删除最值
- 删除任意元素
- 修改任意元素
前三个操作可以采用STL直接实现,然而后两个就不行了。
堆删除最后一层之后是一个完全二叉树,我们以小根堆为例。
双堆找中位数
-
题意:P1168 中位数。给定一个长度为 N 的非负整数序列 A,对于前奇数项求中位数。
-
思路首先记录一个变量mid,记录答案(中位数)。建立两个堆,一个大根堆一个小根堆,大根堆≤mid的数,小根堆存 >mid的的数。所以我们向堆中加入元素时,就通过与mid的比较,选择加入哪一个堆。但我们在输出答案前需要对mid进行调整,如果小根堆和大根堆内元素相同,就无需处理,此时mid仍然是当前的中位数。如果两个堆中元素个数不同,那我们就需要进行调整。具体是把元素个数较多的堆的堆顶作为mid,上一次的mid加入元素较少的堆。
参考:https://www.luogu.com.cn/blog/zy-E/p1168-zhong-wei-shuo
博弈论
-
基本概念
- 先手必胜状态:先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
- 先手必败状态:先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。
-
细节:
-
下面的例子的最终态都是必败态,并且这个最终态都是无求可拿,没有任何选择时就输
-
如果告诉了必败态不是没有选择的情况时,那就控制初始值
例题:lqb真题:取球游戏
-
如果题目告诉了必胜态的情况,要转换成必败态去做。
例题:剪纸游戏
-
-
Nim游戏:石子
-
题意:给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
-
结论:
-
证明
-
拓展
本题还可以求出如果先手必胜,那么输出先手的第一次选择。代码如下
Nim游戏:台阶
其实阶梯博弈经过转换可以变为Nim…把所有奇数阶梯看成N堆石子…做nim…把石子从奇数堆移动到偶数堆可以理解为拿走石子…就相当于几个奇数堆的石子在做Nim
- 结论:奇数项异或和不等于0就是先手必胜
-
拓展
SG函数概念
- SG函数理论:多个独立局面的SG值,等于这些局面SG值的异或和。
SG函数模板
计算每堆石子的所有变化情况:sg函数,再异或
static int[] sg = new int[N];//sg函数值
//sg函数初始化
Arrays.fill(sg,-1);//初始化成负数
//计算节点x的sg函数(记忆化搜索)
static int calSg(int x) {
if(sg[x]!=-1) return sg[x];
HashSet<Integer> se = new HashSet<Integer>();
//具体的逻辑,会变
for(int i=1;i<=k;i++) {
if(x>=set[i])
se.add(calSg(x-set[i]));
}
//计算mex(不在se中的最小的非0元素)
for(int i=0;;i++) {
if(!se.contains(i)) {
return sg[x] = i;
}
}
}
//答案:
for(int i=1;i<=n;i++) {
int x = sc.nextInt();
res ^= calSg(x);
}
if(res!=0) System.out.println("Yes");
else System.out.println("No");
sg函数的例子,如下:集合。
SG函数:有向图游戏
计算k枚棋子的sg值,再异或,记忆化搜索
SG函数:集合
计算sg函数时,枚举整个集合,不需要真的建图
Nim是Sg定理的特例
-
普通的Nim定理其实就是Sg定理的特例,在普通的Nim中,sg(x)=x,所以不同算每个节点x的sg值了,因为就是x,所以就相当于所有值的异或。
在Nim中,有n堆石子,分别画出n棵树,根节点分别是石子的值,下面的子节点都是一次操作,计算n个根节点的sg值,最后异或起来,如果非0就是先手必胜
SG函数:二维子节点的sg值
-
题意:有N堆石子,选择一堆石子(个数是x),把他拿走,然后再添加两堆石子,这两堆石子,每堆石子的最大值是x-1,最小值是0。
-
思路:这是sg函数的题,关键点在于:一堆石子x可以转移很多个子节点状态,每个子节点又是二维的,因为有两堆,表示为(i,j),需要计算这个二维子节点的sg值,又相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。即sg(i,j) = sg(i)^sg(j)
-
题解:https://www.acwing.com/activity/content/code/content/6473987/
SG函数:剪纸游戏
本题是更复杂的二维情况,根节点节点是二维的,每个子节点是两个二维节点
博弈论例题:
-
lqb真题:分石头(sg函数)
https://www.lanqiao.cn/problems/2241/learning/?page=1&first_category_id=1&sort=students_count&tags=%E5%8D%9A%E5%BC%88%E8%AE%BA
-
lqb真题:取数游戏(sg函数+集合)
贪心
- 区间的贪心问题,一般都是上来先按左端点或者右端点排序,方法:考虑区间a被区间b包含的情况,比如区间选点问题,此时就选择小的区间a,再画图对比一下,就知道选择按照右端点;比如区间覆盖问题,此时选择区间大的区间b,就是左端点排序。
区间选点
-
题意:给定 N个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。
-
贪心策略:根据右端点升序
-
算法思路:r是等到ans++修改后才更新
Arrays.sort(arr,1,n+1,(a,b)->(a.b-b.b)); int ans = 1; int r = arr[1].b; for(int i=2;i<=n;i++) { if(arr[i].a > r) { ans ++; r = arr[i].b; } }
最大不相交区间数量
最大不相交区间数==最少覆盖区间点数
-
题意:给定 N个闭区间 [ai,bi],请你在选择若干区间,使得选中的区间之间互不相交。输出可选取区间的最大数量。
-
贪心策略:根据右端点升序
-
算法思路:r是等到ans++修改后才更新
本题代码和区间选点代码完全一样
区间覆盖
-
题意:给定 N个闭区间 [ai,bi],以及一个线段区间[s,t]。请你在选择尽量少的区间,将指定线段区间完全覆盖。
-
贪心策略:根据左端点升序
-
算法思路:双指针找到区间左端点在s前面,右端点的最大值,然后就选择该区间,下一轮的s就变成了当前区间右端点
for(int i=1;i<=n;i++) { int j = i,r = -0x3f3f3f3f;//r是右端点最大值 //找到右端点的最大值 while(j<=n && arr[j].l<=start) { r = Math.max(r,arr[j].r); j++; } //如果最大值还是到不了下一次覆盖的起点,就直接返回 if(r<start) { break; } ans++; start = r; if(r>=end) { flag = 1; break; } i = j-1;//下一轮指定 } 作者:weiambt 链接:https://www.acwing.com/activity/content/code/content/6481372/ 来源:AcWing 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
区间分组
-
题意:给定 N个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小
-
贪心策略:根据左端点升序
-
算法思路:堆维护所有组的右端点最小值。
-
解释:使用小根堆维护所有组的右端点的最小值
在已经存在的组中,没有右端点比当前区间的左端点小的,那么就新开一个组
// 有右端点比当前区间的左端点小的,那么就加到这个组中
//用堆的原因:只需要判断所有组中是否存在一个组的右端点比它小,所以直接维护右端点的最小值即可PriorityQueue<Integer> q = new PriorityQueue<Integer>((a,b)->(a-b)); for(int i=1;i<=n;i++) { PII now = arr[i]; //当前所有组的右端点最小值比左端点大,那么就新建组 if(q.size()==0 || q.peek()>=now.l) { q.add(now.r); ans++; }else {//否则就加入到右端点最小的组中(这里其实加到哪一组都是一样的,为了方便加入到最小的组中) q.poll(); q.add(now.r); }
绝对值不等式
-
题意:在一条数轴上有N个点,A1~AN。寻找一个点,使得n个点到该点的距离和最小
-
不等式:|x-a|+|x-b|>=|a-b|(当且仅当x在线段ab上时取等)
推广:
|x-a1|+|x-a2|+...|x-an| =(|x-a1|+|x-an|)+(|x-a2|+|x-an-1|)+... >=|a1-an|+|a2-an-1|+...(每一个等号成立的条件都是x在范围之间)
所以得出结论:
当n为奇数时,x为n的中位数,即(n+1)/2
当n为偶数时,x取中间两个数之间任意数都可,[n/2,n/2+1] ,方便写代码,这里也可以取(n+1)/2
-
思路:升序排列,一次计算和中间值的差值
耍杂技的牛
推公式:https://www.acwing.com/activity/content/code/content/6500568/
- 贪心推公式的思路:要想到贪心,画表格,相邻两个数,交换前、交换后的值,设定大小关系,使得交换前的最优