归纳0-错题本-算法思想-数位运算(模板)-树状数组(模板)-二分查找(模板)-判断质数(模板)-缓存思想等
1-数位运算
1-1-位图/状态压缩技巧
- 可以用一个数字表示一个数组中所有值的取值与否(从而将不可哈希的boolean数组转换为可以哈希的数字,这样就可以做为key传递给哈希表了),或者记录一个字符串中不同字母是否出现过(int中的第26位分别标记a~z是否出现过,并通过按位与的操作判断两个字符中是否有相同字母),并进行相应的修改
int flag = 0;
int[] nums;
...
for(int i=0; i<nums.length; i++){
if(((flag>>i) & 1) ==1) //判断数组第i位是否用过
flag |= 1<<i //数组第i位被使用了,做标记
flag ^= 1<<i //撤销数组的第i位的使用,撤销标记
}
1-2-lowbit运算
- 树状数组的基础-实质为:取非负整数n在二进制表示下最低位1及其后面的0构成的数值
- 核心代码:
x&(~x+1)
等价于x&(-x)
- 模板:
int lowbit(int x) {
return x & -x;
}
1-3-统计数字二进制表示中1的个数
- 方法1-移位操作
- 思想:逐渐判断2进制每一位是否为1
- 核心代码:
x&(1<<i)
- 模板:
private int countOne(int x){ int ans = 0; for(int i=0; i<32; i++){ //运算优先级:计算,比较,逻辑,位 if((x&(1<<i))!=0){ ans++; } } return ans; }
- 方法2-逐渐将最低为1变为0
- 思想:把n的二进制位中的最低位的1逐渐变为0
- 核心代码:
n&(n-1)
- 模板:
private int countOne(int x){ int ans = 0; while(x!=0){ x=x&(x-1); ans++; } return ans; }
1-4,思想:逐个考虑每一个比特位
- 6065. 按位与结果大于零的最长组合
- 考虑每一个比特位,统计这一位上的 11 的个数,取所有个数的最大值作为答案。
import java.util.Arrays;
class Solution {
public int largestCombination(int[] candidates) {
int[] bitNums = new int[24]; //2^24 == 10^7
for(int candidate: candidates){
int i=0;
while(candidate!=0){
bitNums[i]+=candidate&1;
i++;
candidate = candidate>>1;
}
}
return Arrays.stream(bitNums).max().getAsInt();
}
}
1-5,位运算的性质及应用
1-6,逐渐将最低位零变为1,以及逐渐将最低位1边为0-原数修改
x |= x+1 //逐渐将最低位零变为1
x &= x-1 //逐渐将最低位1边为0
1-6,逐个统计最小1所在的位置
- 2的幂的前缀积->指数的前缀和
- https://leetcode.cn/contest/biweekly-contest-89/problems/range-product-queries-of-powers/
ArrayList<Integer> cache = new ArrayList<>();
int num = n;
int index=0;
while(num!=0){
if(num%2==1){
cache.add(index);
}
index++;
num = num>>1;
}
import java.util.ArrayList;
class Solution {
ArrayList<Integer> cache = new ArrayList<>();
long MOD = (long)(1e9+7);
public int[] productQueries(int n, int[][] queries) {
int num = n;
int index=0;
while(num!=0){
if(num%2==1){
cache.add(index);
}
index++;
num = num>>1;
}
int[] beforeSum = new int[cache.size()];
beforeSum[0] = cache.get(0);
for(int i=1; i<cache.size(); i++){
beforeSum[i] = cache.get(i)+beforeSum[i-1];
}
//快速幂
int[] res = new int[queries.length];
index=0;
for(int[] query: queries){
if(query[0]==0){
// System.out.println(beforeSum[query[1]]);
res[index] = (int)getFastMi(beforeSum[query[1]]);
}else{
// System.out.println(beforeSum[query[1]]-beforeSum[query[0]-1]);
res[index] = (int)getFastMi(beforeSum[query[1]]-beforeSum[query[0]-1]);
}
index++;
}
return res;
}
//快速幂
private long getFastMi(int num){
if(num==0){
return 1L;
}else if(num%2==1){
return getFastMi(num-1) * 2L % MOD;
}else{
long middleRes = getFastMi(num/2) % MOD;
return middleRes*middleRes%MOD;
}
}
}
2-树状数组
- 宫水三叶-关于各类「区间和」问题如何选择解决方案(含模板)
- 针对的问题:给出一个长度为n的数组,要求支持单点修改和查询区间和的操作,两个问题可以具体阐述如下:
- 单点修改:将第x个数加上k(树状数组时间复杂度O(logn))
- 查询区间和:输出区间[x,y]内每个数的和(树状数组时间复杂度O(logn),n次操作的总复杂度为O(nlogn))
- 前置知识-lowbit操作:非负整数n在二进制表示下最低位1及其后面的0构成的数值
int lowbit(int x){
return x&(~x+1);
}
//由于计算机存储时用的是补码,这等价于
int lowbit(int x){
return x&(-x);
}
- 树状数组的思想:区间查询还是用的前缀和相减的方法,只是用树状数组维护前缀和可以将时间复杂度降低为O(logn);并且在存储前缀和时是分块存储的,某个节点数值的修改只需要修改与其相关的父节点即可。
- 树状数组的特点:
- 对于树状数组:每个节点t[x]保存以x为根的子树中叶节点的和(分块后的区间和),每个以节点t[x]为根的子树覆盖的长度为lowbit(x)
- 对于树状数组:每个节点t[x]的父节点为t[x+lowbit(x)],这也是单点修改时所有需要变更值的点
- 对于树状数组:每个节点t[x]前缀和是分块相加求得的,需要从t[x]出发向左上找到每一块的根节点,除了t[x]根节点依次为t[x-lowbit(x)]
- 对于树状数组:整棵树的深度为logn+1
- 树状数组的两个操作
- add(x,k)操作:节点t[x]的值增加k。在整棵树上维护这个值时,不需要修改所有值,而只是一层一层向上找到其父节点t[x+lowbit(x)],并修改各父节点的值即可,最坏复杂度为O(logn)
private void add(int x, int k){ for(int i=x; i<n; i+=lowbit(i)){ tree[i]+=k; } }
- ask(x)操作:查询t[0]-t[x]的前缀和。分块相加求得,各块的根节点为t[x-lowbit(x)]
private int ask(int x){ int ans = 0; for(int i=x; i>0; i-=lowbit(x)){ ans += tree[i]; } return ans; }
- 整体模板
class NumArray {
int[] tree;
int lowbit(int x) {
return x & -x;
}
int query(int x) {
int ans = 0;
for (int i = x; i > 0; i -= lowbit(i)) ans += tree[i];
return ans;
}
void add(int x, int u) {
for (int i = x; i <= n; i += lowbit(i)) tree[i] += u;
}
int[] nums;
int n;
public NumArray(int[] _nums) {
nums = _nums;
n = nums.length;
tree = new int[n + 1];
for (int i = 0; i < n; i++) add(i + 1, nums[i]);
}
public void update(int i, int val) {
add(i + 1, val - nums[i]);
nums[i] = val;
}
public int sumRange(int l, int r) {
return query(r + 1) - query(l);
}
}
3-二分查找
1,写出判定一个值是否可以满足条件的函数g
2,找出左右边界(最小的可能值L,与最大的可能值R)
3,while L<R:
middle = f1(L,R)
if g(middle):
R = middle
else:
L = f2(middle)
return L
//x小于arr.min则返回0,x大于arr.max则返回arr.length,x在arr中则返回对应索引,x不在arr中且不超出arr范围则返回大于x的第一个数的索引
int lowerBound(int[] arr, int x) {
int left = 0, right = arr.length;
while (left < right) {
var mid = (left + right) / 2;
if (arr[mid] >= x) right = mid;
else left = mid + 1;
}
return left;
}
V2-感觉没有上面的稳定
public class Main {
public static void main(String[] args) {
int[] nums = new int[]{1,2,5,8,12};
for(int i=-2; i<=13; i++){
System.out.printf("%d %d \n", i, binarySearch(nums, i));
}
}
static int binarySearch(int[] nums, int target){
//查找的target应在nums的范围内
// 如果target在nums中,则返回在nums中target对应的index
// 如果target不在nums中,返回的值必然最最接近target的索引的其中一个(索引对应的值可能小于target,也可能大于target)
//查找的target如果小于nums的最小值,返回0
//查找的target如果大于nums的最小值,返回nums.length
/*
//排除不在nums范围内的target
if(target<nums[0]){
return -1;
}
if(target><nums[nums.length-1]){
return nums.length;
}
*/
int lo = 0;
int hi = nums.length;
while(lo<hi){
int middle = (lo+hi)>>1;
if(nums[middle]==target){
return middle;
}else if(nums[middle]>target){
hi = middle-1;
}else{
lo = middle+1;
}
}
return lo;
}
}
3-1,一种二分查找的通用形式
- 打补丁
- 针对数组中没有重复值,但是可以有缺失值的情况
- 左闭右开区间,l<r,l=middle+1,r=middle-1,最后打补丁-补丁用l还是r的依据如下
- nums[l]是大于target时离target最近的数字,所以求大于等于且最近的索引时,l++将nums[l]<target的漏洞补上
- nums[r]是小于target时离target最近的数字,所以求小于等于且最近的索引时,r--将nums[r]>target的漏洞补上
- 比如说,所查找的数可能在也可能不在数组内,要求返回大于等于且与所查找的数的值最接近的索引
int[] nums; int target; int l = 0; int r = nums.length; //左闭右开区间 while(l<r){ // 注意while条件 int middle = (l+r)/2; if(nums[middle]==target){ return middle; }else if(nums[middle]<target){ l = middle+1; }else{ r = middle-1; } } //补丁:最终nums[l]有可能小于,等于,大于target(nums[r]也是) //但是nums[l]大于target时一定是离target最近的那个数(满足条件)(同样的nums[r]小于target时一定是离target最近的那个数字) //所以此时只要将nums[l]小于target的情况用补丁补上即可(不满足条件的时候用补丁补上) while(l<nums.length && nums[l]<target){ l++; }
#include <iostream> #include <vector> using namespace std; int main(){ vector<int> v = {1,2,5,8,9,10,11}; int target=5; int step = 0; auto l = v.begin(); cout<<*l<<endl; auto h = v.end(); cout<<*h<<endl; cout<<h-l<<endl; while(l<h){ auto middle = l+(h-l)/2; cout<<*middle<<endl; step++; if(*middle==target){ cout<<"found"; }else if(*middle<target){ l = middle+1; }else{ h = middle-1; } } while(l<v.end() && *l<target){ l++; step++; } cout<<to_string(*l)+" "+to_string(step)<<endl; }
- 左闭右开区间,l<r,l=middle+1,r=middle-1,最后打补丁-补丁用l还是r的依据如下
- 针对数组中有重复值的情况-应该重复值+缺失值通用
- 例题:6096. 咒语和药水的成功对数
- 判别条件是< > <= >=的判别依据如下(=用于有重复值的情况):
- nums[l]是大于等于target中的最小值,所以在求大于等于target的最小值时,l=middle+1的判别条件是nums[middle]<target(因为可能有多个等于target的情况,此时依旧需要r=middle-1进行二分搜索)(或者可以这样考虑:此时判别依据用的是l,所以不到万不得已不要改变l,相等时应该去改变r)
- nums[r]是小于等于target中的最大值,所以在求小于等于target的最大值时,r=middle-1的判别条件是nums[middle]>target
int[] nums; int target; int l = 0; int r = nums.length; //左闭右开区间 while(l<r){ // 注意while条件 int middle = (l+r)/2; if(nums[middle]<target){ //既然是打补丁,那么应该使得l尽可能偏向于左侧,所以用< l = middle+1; }else{ r = middle-1; } } //补丁:最终nums[l]有可能小于,等于(最右侧),大于target(nums[r]也是,只是要改为>=) //但是nums[l]大于target时一定是离target最近的那个数(满足条件)(同样的nums[r]小于target时一定是离target最近的那个数字) //所以此时只要将nums[l]小于target的情况用补丁补上即可(不满足条件的时候用补丁补上) while(l<nums.length && nums[l]<target){ l++; } //r的补丁特殊一些,因为r的初始条件是num.length,而且r可能根本没有变过,所以nums[r]可能会报错,补丁还要再加一层 if(r==nums.length){ r--; } while(r>=0 && nums[r]>target){ r--; }
- 针对数组中没有重复值,但是可以有缺失值的情况
- 如果不打补丁
- 左闭右闭区间,l<=r,l=middle+1,r=middle-1,最后返回值为l/r +-1
- 指导原则依旧是l会是大于/大于等于的索引最小值,r会是小于/小于等于的索引最大值
- 不打补丁是因为有些用二分的题l或r变化后,求得变化后的值都是时间复杂度比较高的
- 719. 找出第 K 小的数对距离
4-判断质数
- 枚举:遍历\([2, \sqrt{n}]\),看是否是因数
- 模板:
private boolean check(int x){ for(int i=2; i*i<=x; i++){ if(x%i==0){ return false; } } return true; }
- 模板:
- 埃氏筛
- 线性筛
5-缓存思想
- 如果能够分析得出解决问题过程中某个环节全部的可能情况(由于题目条件限制导致这种情况是有限的,并且全部的可能情况是容易得到的),可以将其全部的可能情况利用数组、字典等缓存下来,这样就不用写这个环节对应的代码而直接查缓存值即可得到答案。
- 这种思想的极限:面向测试用例编程-试出来所有的测试用例,一个个将对应答案添加到缓存中,根据输入直接返回结果。🐕
- 正常的用法:记忆化搜索(递归+备忘录形式)、备忘录(递归+备忘录形式)、递归树的剪枝(递归+备忘录形式)、动态规划(迭代+dp数组形式)这些本质都是相同的,都是讲中间计算结果缓存下来,防止重复计算。
- 框架
//想好状态,定义好缓存数组
*** dp(***){
//结束条件
//查表
//选择
//存表
//返回
}
- 例题
[6043. 统计包含每个点的矩形数目](https://leetcode-cn.com/contest/weekly-contest-290/problems/count-number-of-rectangles-containing-each-point/)
//建立二维缓存然后直接查表反而是超时的
import java.util.HashMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.ArrayList;
class Solution {
public int[] countRectangles(int[][] rectangles, int[][] points) {
//建立缓存
Arrays.sort(rectangles, (a,b)->(b[1]-a[1]));
HashMap<Integer, ArrayList<Integer>> cache = new HashMap<>();
for(int[] rectangle: rectangles){
if(!cache.keySet().contains(rectangle[1])){
cache.put(rectangle[1], new ArrayList<Integer>());
}
cache.get(rectangle[1]).add(rectangle[0]);
}
ArrayList<Integer> hCache = new ArrayList<>();
for(int h: cache.keySet()){
Collections.sort(cache.get(h));
hCache.add(h);
}
Collections.sort(hCache, (a,b)->(b-a));
//查表
int[] res = new int[points.length];
for(int i=0; i<res.length; i++){
int middleRes = 0;
int presentL = points[i][0];
int presentH = points[i][1];
for(int h: hCache){
if(h>=presentH){
//二分看看对应的有多少个值
middleRes += binarySearch(cache.get(h), presentL);
}
}
res[i] = middleRes;
}
return res;
}
int binarySearch(ArrayList<Integer> nums, int target){
int lo = 0;
int hi = nums.size();
int index = 0;
while(lo<hi){
int middle = (lo+hi)>>1;
if(nums.get(middle)==target){
index = middle;
break;
}else if(nums.get(middle)<target){
lo = middle + 1;
}else{
hi = middle - 1;
}
}
if(index!=0){
return nums.size()-index;
}
if(lo==nums.size()){
return 0;
}
if(nums.get(lo)<target){
return nums.size()-lo-1;
}
return nums.size()-lo;
}
}
[6044. 花期内花的数目](https://leetcode-cn.com/contest/weekly-contest-290/problems/number-of-flowers-in-full-bloom/)
//会超出时间限制,但是认了,代码逻辑是对的。其中二分包括有重复数取第一个的索引,有重复数取最后一个的索引,数组中不存在取最接近的索引,以及数组中不存在防止索引超边界。
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
class Solution {
public int[] fullBloomFlowers(int[][] flowers, int[] persons) {
ArrayList<Integer> cacheStart = new ArrayList<>(); //统计开的时间,并排序
ArrayList<Integer> cacheEnd = new ArrayList<>(); //统计闭的时间,并排序
for(int[] flower: flowers){
cacheStart.add(flower[0]);
cacheEnd.add(flower[1]);
}
Collections.sort(cacheStart);
Collections.sort(cacheEnd);
// System.out.println("start");
// for(int num: cacheStart){
// System.out.println(num);
// }
// System.out.println("end");
// for(int num: cacheEnd){
// System.out.println(num);
// }
// System.out.println("============");
//找人什么时候来的,二分查找(找等于的最后一个)
int[] res = new int[persons.length];
for(int i=0; i<res.length; i++){
int numStart = getIndex(persons[i], cacheStart, true);
int numEnd = getIndex(persons[i], cacheEnd, false);
// System.out.printf("%d %d %d \n", persons[i], numStart, numEnd); //===============
res[i] = numStart - numEnd;
}
return res;
}
int getIndex(int time, ArrayList<Integer> cache2, boolean flag){
//防止索引超边界
if(time<cache2.get(0)){
return -1;
}
if(time>cache2.get(cache2.size()-1)){
return cache2.size()-1;
}
//剩下都不会超出cache2的范围
int lo = 0;
int hi = cache2.size();
while(lo<=hi){
int middle = (lo+hi)>>1;
if(cache2.get(middle)==time){
if(flag){
// System.out.println("====");
// System.out.println(flag);
// System.out.println(time+"====");
while(middle+1<cache2.size() && cache2.get(middle+1)==time){
middle++; //有重复数取最后一个
}
return middle;
}else{
// System.out.println("----");
while(middle-1>=0 && cache2.get(middle-1)==time){
middle--; //有重复数取第一个
}
return middle-1;
}
}else if(cache2.get(middle)<time){
lo = middle + 1;
}else{
hi = middle - 1;
}
}
//time在cache2中不存在,lo是最接近time的数的索引,但是大于time还是小于time不一定
if(cache2.get(lo)>time){
return lo - 1;
}
return lo;
}
}
6-积尾随零的数量
- 是所有乘数中因子 2 的数量与因子 5 的数量之中的较小值。
- https://leetcode-cn.com/problems/maximum-trailing-zeros-in-a-cornered-path/
7-蓄水池抽样
- 问题:给你一个可能含有 重复元素 的整数数组 nums ,请你随机输出给定的目标数字 target 的索引。你可以假设给定的数字一定存在于数组中。
- 算法原理:遍历nums,当我们第i次遇到值为target的元素时,随机选择区间[0,i)内的一个整数,如果其等于0,则将返回值置为该元素的下标,否则返回值不变。假设 nums 中有k个值为target的元素,该算法会保证这k个元素的下标成为最终返回值的概率均为
1/k
。 - 应用场景:若 nums 并不是在初始化时完全给出,而是持续以「流」的形式给出,且数据流的很长,不便进行预处理的话,我们只能使用「蓄水池抽样」的方式求解。
- 资料:
8-模运算规律
- 大佬总结的取模运算的性质
- 交换律
- (a + b) mod p = (b + a) mod p
- (a × b) mod p = (b × a) mod p
- 结合律
- ((a+b) mod p + c)mod p = (a + (b+c) mod p) mod p
- ((ab) mod p * c)mod p = (a * (bc) mod p) mod p
- 分配律
- (a×b) mod c=(a mod c * b mod c) mod c
- (a+b) mod c=(a mod c + b mod c) mod c
- (a-b) mod c=(a mod c - b mod c) mod c
- ((a + b)mod p × c) mod p = ((a × c) mod p + (b × c) mod p) mod p
- 解题时有时候结果太大,不但最后结果要取模,中间的计算结果也要取模,例子如下:
- 6058. 统计打字方案数
解法案例
import java.util.ArrayList;
class Solution {
public int countTexts(String pressedKeys) {
long res = 1; //记录结果
int i = 0; //记录字符串索引
int subStringLength = 1; //记录相同字符的子字符串长度
char presChar = pressedKeys.charAt(i);
while(++i<pressedKeys.length()){
if(presChar==pressedKeys.charAt(i)){
subStringLength++;
}else{
//通过subStringLength统计可能性
res = getMod(res, getCombNum(subStringLength, presChar)); //=====相乘阶段先取余
//res *= getCombNum(subStringLength, presChar);
//初始化
subStringLength = 1;
presChar = pressedKeys.charAt(i);
}
}
res = getMod(res, getCombNum(subStringLength, presChar)); //=====相乘阶段先取余
//res *= getCombNum(subStringLength, presChar);
return (int)(res%((long)1e9+7));
//return (int)res;
}
long getCombNum(int subStringLength, char presChar){
ArrayList<Long> candNum = new ArrayList<>();
int choiceNum = 3;
candNum.add(0L);
candNum.add(1L);
candNum.add(2L);
candNum.add(4L);
if(presChar=='7' || presChar=='9'){
candNum.add(8L);
choiceNum = 4;
}
//求所有的组合数,应该怎么求呢?-状态转移
if(subStringLength+1<=candNum.size()){
return candNum.get(subStringLength);
}
while(candNum.size()!=subStringLength+1){
long addNum = 0;
for(int i=1; i<=choiceNum; i++){
addNum = getMod2(addNum, candNum.get(candNum.size()-i)); //=====相加阶段先取余
//addNum += candNum.get(candNum.size()-i);
}
candNum.add(addNum);
}
return candNum.get(subStringLength);
}
long getMod(long inputNum1, long inputNum2){
return (inputNum1%((long)1e9+7) * inputNum2%((long)1e9+7)) %((long)1e9+7);
}
long getMod2(long inputNum1, long inputNum2){
return (inputNum1%((long)1e9+7) + inputNum2%((long)1e9+7)) %((long)1e9+7);
}
}
9-大佬的与数学相关的解题笔记
10-求凸包-587. 安装栅栏
- 凸包-维基百科
- Jarvis 算法、Graham 算法、 Andrew 算法
11-三角形面积计算
S=(1/2)*(x1*y2+x2*y3+x3*y1-x1*y3-x2*y1-x3*y2)
- 用途:
- 计算三角形面积
- 判断三点共线(面积为零,则代表三点在一条线上)
12-单调栈
- 针对问题:需要求出每个值与上一个更大、小元素之间的下标之差
- 单调栈本身就是一个与特定数组对应的,从栈底到栈顶的元素是单调变化的栈。本身数据结构的思想很简单,需要和具体问题配合使用才能发挥其威力。
- 思考逻辑:
- 从哪侧遍历:左侧的从左端点开始遍历,右侧的从右端点开始遍历
- pop条件看什么条件满足扩张(同时要根据题意想好要不要带等号,是两侧都不带,还是一侧带一侧不带,还是两侧都带)
- left/right索引添加
- stack添加值
- 代码示例:
int[] nums;
Deque<Integer> stack = new ArrayDeque<Integer>();
//从栈顶到栈底单调递增
for(int i=nums.length-1; i>=0; i--){
//重点1-从nums哪侧开始遍历
while(!stack.isEmpty() && nums[i]>stack.peek()){
//重点2-先出栈再入栈
stack.pop()
}
stack.push(nums[i]);
}
- 用途:求数组nums1中各元素在nums2中的下一个大于的值/值对应的坐标,下一个小于的值/值对应的坐标,上一个大于的值/值对应的坐标,上一个小于的值/值对应的坐标等等。(nums1为nums2的子列,或者就是nums2本身)
- 通过预处理nums2生成单调栈,并借助缓存,将nums1中各元素的查询时间复杂度由O(N)降低到O(1)。
- 例题:
13-滑动窗口
- 双指针类算法的一种,算法技巧的思路非常简单,就是维护一个窗口,在未搜索结束时,满足扩张条件则右指针右移,满足收缩条件则左指针右移
- 模板
int l = 0;
int r = 0;
int res = 0;
int pres = 0;
while(l<搜索范围 && r<搜索范围){
//满足搜索条件
if(扩张条件成立){
//更新pres
//更新res
r++;
}
if(收缩条件成立){
//更新pres(如有需要)
//更新res(如有需要)
l++;
}
}
14-前缀和
- 普通的前缀和很容易,思想也很简单,就是用来做区间求和查询的,可以将时间复杂度降为0(1),但是进阶的就需要自己结合数学会推导公式了(也叫容斥原理)
- 比如这样一个例子:2281. 巫师的总力量和,解题的其中一个关键可以归结为:如何在O(1)时间内得到一个区间内包含固定位数字的所有非空连续子序列和的和
- 考虑这样一个问题:对于一个数组
int[] nums
,能否在O(1)时间复杂度内求得数组nums
的包含nums[i]
所有非空连续子序列和的和
- 考虑这样一个问题:对于一个数组
\[记nums的前缀和为s,要求的是所有包含第i位nums[i]的非空连续子序列和的和,数组的长度为R,公式如下(含义就是以i为分界点,l和r取遍所有的可能组合)
\]
\[\sum_{l=0}^{i-1}\sum_{r=i}^{R-1}(s[r]-s[l])
\]
\[l的变化与s[r]无关,就可以化进去
\]
\[\sum_{r=i}^{R-1}[(i-1-0+1)s[r]-\sum_{l=0}^{i-1}s[l]]=\sum_{r=i}^{R-1}[i*s[r]-\sum_{l=0}^{i-1}s[l]]
\]
\[r的变化也与s[l]无关,也可以化进去
\]
\[\sum_{r=i}^{R-1}[i*s[r]-\sum_{l=0}^{i-1}s[l]]=i*\sum_{r=i}^{R-1}s[r]-(R-1-i+1)\sum_{l=0}^{i-1}s[l]=i*\sum_{r=i}^{R-1}s[r]-(R-i)*\sum_{l=0}^{i-1}s[l]
\]
\[如果记s的前缀和为ss(前缀和的前缀和),则上式可化为
\]
\[i*\sum_{r=i}^{R-1}s[r]-(R-i)*\sum_{l=0}^{i-1}s[l]=i*(ss[R-1]-ss[i-1])-(R-i)*(ss[i-1])=i*ss[R-1]-R*ss[i-1]
\]
- 上述过程的最终结果不重要,这种写公式推导化简的能力很关键(因为左右边界可以发生变化,比如和单调栈结合)
15-快速幂
- 要计算\(m^{n}\),则有下式:
\[m^{n}=\begin{cases}
(m^{|n/2|})^2 & n为偶数\\
m(m^{|n/2|})^2 & n为奇数
\end{cases}
\]
- 同时结合
\[(a*b)\%c=((a\%c)*(b\%c))\%c
\]
- 代码
public class test {
/**
* 快速幂,时间复杂度O(logn)
* @param m 底数
* @param n 指数
* @return 幂
*/
static long QuickPow(int m, int n){
long ans = 1;
long base = m;
while(n!=0){
if((n&1)!=0){
ans = ans * base;
}
base = base * base;
n = n>>1;
}
return ans;
}
/**
* 快速幂并求模
* @param m 底数
* @param n 指数
* @param p 除数
* @return 求模后的幂
*/
static long QuickPowRow(int m, int n, int p){
long ans = 1;
long base = m;
while(n!=0){
if((n&1)!=0){
ans = (ans * (base % p)) % p; //这里ans不求模,因为ans%p = ans
}
base = ((base % p) * (base % p)) % p;
n = n>>1;
}
return ans;
}
@Test
public void QuickPowTest(){
System.out.println(QuickPow(2, 7));
System.out.println(QuickPowRow(2, 7, 3));
System.out.println(QuickPowRow(1234, 1234, 789));
BigInteger ans = new BigInteger("1");
BigInteger i = new BigInteger("1234");
for(int j=0; j<1234; j++){
ans = ans.multiply(i);
}
System.out.println(ans);
System.out.println(ans.toString().length());
System.out.println(ans.mod(new BigInteger("789")));
}
}
16-运行过程中要不断分割子数组-要想到用双指针
17-节省空间-在原数据上做标记
- 用做标记的数据除了因为做标记时的修改外,应该是最后修改的
- https://leetcode.cn/submissions/detail/368667272/
18-原位替换
a ^= b;
b ^= a;
a ^= b;
19-while遍历时时刻想到索引不要超出范围
20-使用LCP(最长公共前缀长)快速判断两个子字符串是否相等
LCP[i][i+j]>=j #说明s[i:i+j]和s[i+j:i+2*j]两个子字符串是相等的
21-基于索引数组排序(得到排序后结果并且不打乱原数组的排序)
int[] nums = {...};
Integer[] indexes = new Integer[nums.length];
for(int i=0; i<nums.length; i++){
indexes[i] = i;
}
Arrays.sort(indexes, (i,j)->nums[i]-nums[j]);
for(int index: indexes){
System.out.println(nums[index]);
}
22-结果要求取余
- 如果相加,直接取余
(a+b)%MOD
- 如果相减,记得加一下MOD(防止相减时出现负数),前提条件
2*MOD<Integer.MAX_VALUE
((a-b)%MOD+MOD)%MOD
23-快速幂并且结果取余
- https://zhuanlan.zhihu.com/p/95902286
- https://leetcode.cn/contest/biweekly-contest-89/problems/range-product-queries-of-powers/
long MOD = (long)(1e9+7);
private long getFastMi(int num){
if(num==0){
return 1L;
}else if(num%2==1){
return getFastMi(num-1) * 2L % MOD;
}else{
long middleRes = getFastMi(num/2) % MOD;
return middleRes*middleRes%MOD;
}
}
24-并查集模板
class UnionFind{
int[] parent;// parent[i]表示i这个元素指向的父亲节点
int[] size;//size[i]表示以i为根节点的集合中元素个数
int n;//节点的个数,初始化每一个节点都是一个单独的连通分量
int setCount;//连通分量的数目
public UnionFind(int n){
this.size=new int[n];
this.parent=new int[n];
this.n=n;
this.setCount=n;
Arrays.fill(size,1);
for(int i=0;i<n;i++){
parent[i]=i;
}
}
public int find(int x){
return parent[x]==x?x:find(parent[x]);
}
public boolean unit(int x,int y){
x=find(x);
y=find(y);
if(x==y){
return false;
}
if(size[x]<size[y]){
int tem=x;
x=y;
y=tem;
}
parent[y]=x;
size[x]+=size[y];
--setCount;
return true;
}
public boolean connected(int x, int y) {
x = find(x);
y = find(y);
return x == y;
}
}
25-二分法解决最(大/小)值最(小/大)化问题
26-除法上取整
(a+b-1)/b
N-1-常用算法技巧
- 类1
- 二分
- 滑动窗口
- 双指针(快慢指针)
- 单调栈
- 前缀和
- 差分
- 类2
- 回溯
- BFS
- 递归+缓存+状态压缩
- 动态规划
- 类3
- 树状数组
- 线段树
- 并查集
- LCP
N-2-常见数据结构的性质
行动是治愈恐惧的良药,而犹豫拖延将不断滋养恐惧。