💻
QMMMS的笔记
博客
  • QMMMS的笔记
  • agent
    • MCP的背景、原理和开发
    • Agent 历史与背景
    • Agentic Workflows
    • 环境检查与基础工具
    • Tool Call
    • 工具与运行时的值
    • temp
    • 处理 Tool Call error
    • trick
  • algorithm
    • 线性结构
    • 二叉树
    • 图
    • 查找
    • 排序
    • 动态规划
    • 优化方法
    • 数学
    • 迁移至Java
  • computer_composition
    • 系统总线
    • 存储器
    • 输入输出系统
    • 计算机的运算方法
    • 指令系统
    • 补充
  • computer_network
    • 引入
    • 应用层
    • 传输层
    • 网络层(数据平面)
    • 网络层(控制平面)
    • 链路层
    • 常见问答
    • 实验
  • database
    • SQL实战
    • 关系代数
    • 数据库设计
    • 规范化
    • 数据库基本概念
    • 查询原理
    • 数据库恢复技术
    • 并发控制
  • dev_tools
    • Git
    • Nginx
    • Spring
    • LangChain
    • PyTorch Cheat Sheet
    • MyBatis
    • MySQL Cheat Sheet
    • MySQL 补充
    • Redis
    • Docker
    • RocketMQ
    • Chrome
  • linux
    • Linux基础命令与使用
    • 文件与权限
    • 文件与目录操作
    • 权限属性高级
    • 命令与文件的查找
    • 文件压缩和打包
    • vim编辑器
    • shell变量
    • 命令补充
    • 数据流重定向
    • 管道命令
    • shell脚本
    • 用户管理
    • 用户间交流
    • 计划任务
    • 进程管理
    • 软件管理
    • 认识系统服务
    • 运维常用命令
    • 常用命令
  • llm
    • 大规模语言模型概述
    • 分布式训练概述
    • 有监督微调概述
    • 强化学习与LLM
    • LLM评估概述
    • 大模型应用
    • 理解大模型
    • 量化
    • 预训练
    • 上下文学习
  • machine_learning
    • 引入
    • 大致正确学习
    • 一致收敛
    • 偏差还是过拟合?
    • 可学习的充要条件
    • 非均匀可学习性
    • 计算复杂性
  • mathematics
    • 概率与统计基础
    • 线性代数基础
  • operating_system
    • 操作系统基本概念
    • 进程和线程
    • 同步,互斥与死锁
    • 内存管理
    • 文件系统
    • I/O系统
    • 保护与安全
    • 《现代操作系统》
  • statistical_learning
    • 统计学习引入
    • 线性回归
    • 分类
    • 重抽样方法
    • 线性模型选择与正则化
    • 非线性模型
    • 基于树的方法
    • 支持向量机
    • 无指导学习
    • 马尔科夫链和蒙托卡罗方法简明理解
    • R语言速查
  • deep_learning
    • basic_concepts
      • 逻辑回归与损失函数
      • 神经网络
      • 正则化、预处理、权重初始化
      • 优化算法
      • 机器学习策略
      • 复习:从计算机视觉的角度
      • 卷积神经网络
      • 深度卷积网络示例
      • 计算机视觉任务
      • 循环神经网络
      • 自然语言处理任务
      • 注意力
      • Transformers 家族
      • 显卡扫盲
      • 强化学习概述
    • semi-supervise
      • 半监督学习简介
      • Consistency Regularization
      • Proxy-label Methods
      • Holistic Methods
      • Generative Models
      • Graph-Based SSL
      • Self-Supervision for SSL
      • Other SSL methods
  • programming
    • cpp
      • STL
      • C++基础
      • 内存管理
      • 面向对象
    • java
      • 环境和介绍
      • 注释
      • String
      • 面向对象思想
      • Object
      • 包
      • 访问权限修饰符
      • 初始化块
      • 接口
      • 内部类
      • 注解
      • 枚举
      • 集合框架
      • List
      • Map
      • 泛型
      • 迭代
      • IO与流
      • 序列化
      • 异常
      • Lambda
      • Stream流
      • Socket
      • 缓冲
      • 命名规范
      • 拆箱装箱
      • 值传递
      • 深拷贝
      • 反射
      • JVM
      • 并发编程基础
    • python
      • 并发编程
      • 环境管理
  • software_engineering
    • basic_concepts
      • 系统分析与设计概述
      • 规划
      • 需求分析与原型设计
      • 项目管理
      • 建模
      • 数据库设计
      • 架构
      • 配置管理
      • 测试管理
      • 安全
      • 编码原则
      • 微服务
      • 补充内容
    • software_testing
      • CMMI基础
      • PPQA与SQA
      • 软件测试基础
      • 黑盒测试
      • 白盒测试
      • 集成测试
      • 系统测试
      • 测开面试补充
由 GitBook 提供支持
在本页
  • 重新思考排序
  • 二分
  • 二维前缀和
  • 差分
  • 单调队列
  • 并查集
  • 异或
  • 线段树
  • 回溯
  • 杂题思路
在GitHub上编辑
  1. algorithm

优化方法

重新思考排序

任务:

给定一组数字段,连接成最小的数字。例如,给定{32,321,3214,0229,87},我们可以连接成许多数字,例如32-321-3214-0229-87或0229-32-87-321-3214,关于这些片段的不同组合顺序,最小的数字是0229-321-3214-32-87。

分析:

朴素的贪心的思考方式应该是:首先给我两个数字段,例如32和321,我把它们正反组合排列起看哪个更小来排序,在这个例子中是 32-132,然后再拿到3214,在两个数中间的三个空之间插入排序,把3214插入进去。

众所周知,插入排序是很慢的,我们能不能使用快速排序来书写?这里我们遇到了一个困境:我们目前的方式是在每个空位插入一个新的数字段,然后看整个数字哪个最小。而传统的cmp函数方法要求我们随便拿出两个元素出来就要比大小。

我们重新审视排序过程:我们真的需要把整个数字拿出来比较吗?其实我们可以看作把前面和后面的数字固定不动,只比较要插入的这个数字段和我们要比较的数字段,即:

// 或者使用Java的 Comparator
bool cmp(string a, string b){
    return a + b < b + a;
}

来源:PAT甲级 1038 Recover the Smallest Number

二分

任务:

你找到一本残缺很多但还是很厚的词典,想找到某个词或确认它不在,要求时间复杂度$O(log \ n)$

分析:

词典意味着有序

先翻到词典中间,与要找的词比较,让查找范围缩小一半

重复操作,一直到找到

核心代码:

int l=1,r=MAX,mid=(l+r)>>1;//准备开始二分
while(l <= r){//终止条件
	//使用judge函数来判断当前猜测的答案是否可行
    if(judge(mid)){//如果可以的话 
        r = mid - 1;//改变右端点,看看再少一点是不是也可以
        mid = (l + r) >> 1;
    } else{
        l = mid + 1;//当前答案不可行,需要增大答案
        mid = (l + r) >> 1;
    }
}
cout << l << endl;//输出找到的答案

看到最大的最小(最小的最大),可能是二分或者二分答案

std::lower_bound() 返回有序表里第一个大于等于给定值的指针或迭代器;

std::upper_bound() 返回有序表里第一个大于给定值的指针或迭代器;

set / map / … (迭代器是不可减)用法:

a.lower_bound(2)

vector (迭代器是可以减的)用法:

lower_bound(a.begin(), a.end(), 2)

二维前缀和

任务:

给出一个 $n * n$ 矩阵,从中找到最大加权矩形。

分析:

朴素的想法是对所有可能的矩形都做一遍遍历,但是要枚举所有矩形,时间复杂度已经达到 $O(n^4)$,再遍历矩形求权,时间复杂度最终达到 $O(n^6)$

前缀和的思想是记录数组中从开始到当前值的和,做一遍预处理,以应对之后对于和的询问。对于这个任务,可以让时间复杂度保留在 $O(n^4)$

二维前缀和记录左上角顶点为$(0,0)$,右下角顶点为$(i,j)$矩阵的权,递推式为:

f[i][j]=a[i][j]+f[i−1][j]+f[i][j−1]−f[i−1][j−1]f[i][j]=a[i][j]+f[i-1][j]+f[i][j-1]-f[i-1][j-1]f[i][j]=a[i][j]+f[i−1][j]+f[i][j−1]−f[i−1][j−1]

代码:

例题:洛谷P1719 最大加权矩形

https://www.luogu.com.cn/problem/P1719

#include<iostream>
#include<cmath>
using namespace std;

int a[200][200];
int f[200][200];
int N, ans;

int main() {
	cin >> N;
	for (int i = 1; i <= N; ++i) for (int j = 1; j <= N; ++j) cin >> a[i][j];
	for (int i = 1; i <= N; ++i) for (int j = 1; j <= N; ++j) f[i][j] = a[i][j] + f[i][j - 1] + f[i - 1][j] - f[i - 1][j - 1];
	for (int i = 1; i <= N; ++i) for (int j = 1; j <= N; ++j) for (int x = i; x <= N; ++x) for (int y = j; y <= N; ++y) ans = max(ans, f[x][y] - f[i - 1][y] - f[x][j - 1] + f[i - 1][j - 1]);
	cout << ans << endl;
	return 0;
}

差分


任务:

给定一个长度为 $n$ 的数列 ${a_1,a_2,\cdots,a_n}$,每次可以选择一个区间$[l,r]$,使这个区间内的数都加 $1$ 或者都减 $1$。问至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列有多少种。

分析:

差分的思想是记录原数组 $a$ 相邻元素的差,公式就是$s[i]=a[i]-a[i-1]$

对于区间操作可以大幅加速时间,如果要对原数组的$[l,r]$区间$+k$,可以修改$s[l]+=k$,$s[r+1]-=k$,再对差分数组 $s$ 做一遍前缀和,就可以得到区间操作后的原数组,把遍历的$O(n)$降到$O(1)$

对于这个任务,先将其做差分,然后所有正偏差、负偏差求和再取绝对值,设分别为$pos$,$neg$,易得:

最少操作次数=min(pos,neg)+abs(pos−neg)最少操作次数=min(pos, neg) + abs(pos - neg)最少操作次数=min(pos,neg)+abs(pos−neg)
数列种类=1+abs(pos−neg)数列种类=1 + abs(pos - neg)数列种类=1+abs(pos−neg)

代码:

例题:洛谷P4552 [Poetize6] IncDec Sequence

https://www.luogu.com.cn/problem/P4552

#include <iostream>
#include <cmath>
using namespace std;

const int MAX = 1e5 + 5;
int a[MAX], s[MAX];
int N;
long long pos, neg;

int main() {
    cin >> N;
    for (int i = 1; i <= N; ++i) cin >> a[i];
    for (int i = 1; i <= N; ++i) s[i] = a[i] - a[i - 1];
    for (int i = 2; i <= N; ++i) {
        if (s[i] > 0) pos += s[i];
        else neg += -s[i];
    }
    cout << min(pos, neg) + abs(pos - neg) << endl;
    cout << 1 + abs(pos - neg) << endl;

    return 0;
}

单调队列

任务:

有一个长为 $n$ 的序列 $f$,以及一个大小为 $k$ 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

分析:

朴素的想法是每滑动一次就遍历一遍窗口中的所有元素,但这太慢了

考虑维护一个双向队列,它是单调的,最大的在前面,从前到后变小,目标是处理窗口中的最大值

在滑动后,需要处理一个新的值,位置为 $x$,需要把 $f[x]$ 放到队列尾

因为目标是窗口中的最大值,所以位置在 $x$ 之前且比 $f[x]$ 小的数都可以舍弃

更新后,队列最前端的数可能已经不在窗口内,检查并去除

这样,因为队列单调,则最前面的就是窗口内的最大值

核心代码:

例题:洛谷P1886 滑动窗口 /【模板】单调队列

https://www.luogu.com.cn/problem/P1886

deque<pair<int, int>> maxx;	//保存值和位置
int f[MAX];					//序列f

void process_max(int x) {//处理位置为x的数
	while (!maxx.empty() && maxx.back().first < f[x])maxx.pop_back();//之前且比f[x]小的数都可以舍弃
	maxx.push_back(make_pair(f[x], x));//把 f[x] 放到队列尾
	while (!maxx.empty() && maxx.front().second <= x - K)maxx.pop_front();//队列最前端的数可能已经不在窗口内
}

并查集

任务:

有一群人,有些人互相为亲戚,亲戚的亲戚也是亲戚,现在指定两个人,问他们是否是亲戚,要求预处理时间 $O(n)$ ,查询时间 $O(1)$

分析:

换句话讲,在一个图中,查询两点是否联通,要求预处理时间 $O(n)$ ,查询时间 $O(1)$

以亲戚为例,假设每个家族都有一个族长,象征这个家族,数组 $fa[i]$ 代表 $i$ 的长辈(爸爸或爷爷或...或族长)

首先假设每个人都是族长

然后考虑一对父亲和儿子,那么父亲的长辈肯定是儿子的长辈

怎么找到族长?找爸爸的爸爸的爸爸......一直到族长,查询时压缩路径使时间逼近 $O(1)$

族长相同,肯定是亲戚

代码:

例题:洛谷P1551 亲戚

https://www.luogu.com.cn/problem/P1551

#include<iostream>
using namespace std;

int N, M, P;
int fa[5003];
int S, F;

int getfa(int n) {
    if(fa[n] == n) return n;//找到族长
    return fa[n] = getfa(fa[n]);//找爸爸的爸爸,压缩路径
}

void setfa(int son, int father) {
    fa[getfa(son)] = getfa(father);//父亲的长辈是儿子的长辈
}

int main() {
    cin >> N >> M >> P;
    for (int i = 1; i <= N; ++i) fa[i] = i;//假设每个人都是族长
    for (int i = 0; i < M; ++i) {
        cin >> S >> F;
        setfa(S, F);
    }
    for (int i = 0; i < P; ++i) {
        cin >> S >> F;
        if (getfa(S) == getfa(F)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    return 0;
}

进阶

异或


基础:

^的异或指的是二进制中,对应的对应二进制位相同时异或为零,相异时异或为一

  • 规律一:大小相同的数字异或结果一定为0,而0与任何数字进行异或大小不会改变。比如3 ^ 3=0, 3 ^ 0=3

  • 规律二:自反性。A^B^B=A

  • 规律三:二进制的异或运算符合结合律和交换律

任务:

非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。实现线性时间复杂度,只使用常量额外空间的算法。

代码:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
		int ans = nums[0];
         for (int i = 1; i < nums.size(); i++)ans = ans ^ nums[i];
         return ans;
    }
};

线段树


任务:

对一个数列的两种操作:

  1. 将某区间每一个数加上 $k$,要求$O(log, n)$时间复杂度

  2. 求出某区间的加和,要求$O(1)$时间复杂度(接近)

例题:洛谷P3372 【模板】线段树 1

https://www.luogu.com.cn/problem/P3372

回溯

任务:

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

class Solution {
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int> > res;
        vector<int> path;
        dfs(candidates, 0, target, path, res);
        return res;
    }

    void dfs(vector<int>& candidates, int begin, int target, vector<int>& path, vector<vector<int> >& res){
        if(target == 0){
            res.push_back(path);
            return;
        }
        for(int i = begin; i < candidates.size(); i++){
            if(target - candidates[i] < 0){
                continue;
            }
            path.push_back(candidates[i]);
            dfs(candidates, i, target - candidates[i], path, res);
            path.pop_back();
        }
    }
};

出处:力扣39https://leetcode.cn/problems/combination-sum/

杂题思路


一个文件有9998个数,对应[1-10000]的范围,少了两个数。内存1k,怎么查找?先用公式n(n+1)/2算出1-10000的总和 , 用公式n(n+1)(2n+1)/6算出1-10000的平方和,然后扫描那9998个数,每扫到一个就从平方和中减去这个数的平方,扫描一个就从总和中减去这个数。 知道两个数的和和平方和,解方程组即可得出这两个数


求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。递归。


给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

思路:前缀和 + 哈希表,键是前缀和,值是这个前缀和出现的次数。遍历 i,从哈希表中找 pre[j−1]pre[j-1]pre[j−1] 出现的次数

pre[j−1]==pre[i]−kpre[j-1]==pre[i]-kpre[j−1]==pre[i]−k

找出下一个排列:

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。

  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。

  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

标准的 “下一个排列” 算法可以描述为:

  1. 从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序

  2. 在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。

  3. 将 A[i] 与 A[k] 交换

  4. 可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序

  5. 如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4


给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。例如s = ")()())"的最长有效括号子串是 "()()"


一个数组由两段有序序列拼接得来,在 O(log n) 时间内找到目标 target 返回下标

上一页动态规划下一页数学

最后更新于8个月前

并查集并不支持删点操作,我们可以把每个查询都存下来,反过来往里面添加点。

出处:力扣

:现在有包含很多个样本数据的数组,现在对这些数进行多次有放回采样,但每个数都有自己的采样概率,这个概率存在另外一个数组里面,总概率是1,问怎么实现这个采样过程。思路是:设数组 w 的权重之和为 total。根据题目的要求,我们可以看成将 [1,total] 范围内的所有整数分成 n 个部分。给出一个随机数,看落在哪个部分,优化考虑前缀和 + 二分查找。

:贪心,两次遍历

:两次二分,第一次找到两段有序序列的分界点(l=1)

2021 RoboCom 世界机器人开发者大赛T4
136. 只出现一次的数字
按照权重的概率取随机采样
思路
思路