算法模板整理


一.基础算法

1.1前缀和

作用:快速求出原数组中一段数的和

  • 下标一定从1开始 s0=0
  • si=a1+a2+…ai
  • si=si-1+ai

一维前缀和:

 for (int i = 1; i <= n; i ++ )scanf("%d",&a[i]);
    for(int i=1;i<=n;i++)s[i]+=a[i]+s[i-1];

二维子矩阵的和:

  • S[i, j] = 第i行j列格子左上部分所有元素的和:

    s[i,j]=s[i-1,j]+s[i,j-1]-s[i-1,j-1]+a[i,j]

  • 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
    S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

#include<iostream>
using namespace std;

const int N = 1e4 + 10;
int a[N][N], s[N][N];
int m, n, q;
int main()
{
    scanf("%d%d%d", &n, &m, &q);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]);

    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] + a[i][j];
    while (q--)
    {
        int x1, x2, y1, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }
}

1.2差分

作用:操作区间的加减

  • 前缀和的逆运算
  • 构造一个b数组,是a的差分,a是b的前缀和
  • a[i]=b[1]+ … +b[i]
  • b[1]=a[1],b[3]=a[3]-a[2] ,b[4]=a[4]-a[3]
  • a数组[l,r]中数字全部加上c,简化为b[l]+c,b[r+1]-c
int m, n;
int a[N];
int b[N];//差分数组
int main()
{
    scanf("%d%d", &n, &m);;
    for (int i = 1; i <= n; i++)scanf("%d", &a[i]), b[i] = a[i] - a[i - 1];
    int l, r, c;
    while (m--)
    {
        scanf("%d%d%d", &l, &r, &c);
        b[l] += c;
        b[r + 1] -= c;
    }
    for (int i = 1; i <= n; i++)
    {
        a[i] = a[i - 1] + b[i];
        printf("%d ", a[i]);
    }
}

二维差分:给其中的一个子矩阵加上一个值

#include<iostream>
using namespace std;
int m, n, q;
const int N = 1e4 + 10;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c)
{
	b[x1][y1] += c;
	b[x2 + 1][y1] -= c;
	b[x2 + 1][y2 + 1] += c;
	b[x1][y2 + 1] -= c;
}
int main()
{
	scanf("%d%d%d", &n, &m, &q);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
		{
			scanf("%d", &a[i][j]);
			//计算b[i][j];
			insert(i, j, i, j, a[i][j]);
		}
	int x1, y1, x2, y2, c;
	while (q--)
	{
		scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
		insert(x1, y1, x2, y2, c);
	}
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= m; j++)
		{
			//计算前缀和
			a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
			printf("%d ", a[i][j]);
		}
		cout << endl;
	}
}

1.3二分

分为二分答案和二分查找

1.3.1整数二分

注意边界问题:

版本1
当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。

版本2
当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。

bool check(int x) {/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

1.3.2浮点数二分

较简单,不需要考虑边界问题

double bsearch_3(double l,double r )
{
    const double eps = 1e-8;
    while(r-l>eps)
    {
        double mid = (l+r) / 2;
        if(check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

1.4.高精度

1.4.1加法

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;

const int N = 1e6 + 10;
vector<int> add(vector<int>& A, vector<int>& B) {
	vector<int> C;
	int t = 0;

	for (int i = 0; i < A.size() || i < B.size(); i++) {
		if (i < A.size())
			t += A[i];
		if (i < B.size())
			t += B[i];
		C.push_back(t % 10);
		t /= 10;		//看t是否需要进位

	}
	if (t)
		C.push_back(1);		//进位
	return C;
}

int main() {
	string a, b;
	vector<int> A, B;

	cin >> a >> b; 
	for (int i = a.size() - 1; i >= 0; i--)
		A.push_back(a[i] - '0');
	for (int i=b.size() - 1; i >= 0; i--) {
		B.push_back(b[i] - '0');
	}

	auto C = add(A, B);

	for (int i = C.size() - 1; i >= 0; i--) {
		cout << C[i];
	}
}

1.4.2减法

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
vector<int> A;
vector<int> B;
//判断是否有 A>=B
bool cmp(vector<int> A, vector<int> B) {
	if (A.size() != B.size())
		return A.size() > B.size();
	for (int i = A.size()-1; i >= 0; i--) {
		if (A[i] != B[i])
			return A[i] > B[i];
	}
	return true;
}

vector<int> sub(vector<int> A, vector<int> B) {
	vector<int> C;
	int t = 0; //t为借位
	for (int i = 0; i < A.size(); i++) {			
		int temp = A[i] - t;
		if (i < B.size())
			temp -= B[i];
		C.push_back((temp + 10) % 10);
		if (temp < 0)
			t = 1;
		else
			t = 0;
	}
	while (C.size() > 1 && C.back() == 0)	//去前导0
		C.pop_back();
	return C;
}

int main() {
	string a, b;
	cin >> a >> b;
	for (int i = a.size() - 1; i >= 0; i--) {
		A.push_back(a[i] - '0');
	}
	for (int i = b.size() - 1; i >= 0; i--)
		B.push_back(b[i] - '0');
	if (cmp(A, B)) {
		auto C = sub(A, B);
		for (int i = C.size() - 1; i >= 0; i--)
			cout << C[i];
	}
	else {
		auto C = sub(B, A);
		cout << "-";
		for (int i = C.size() - 1; i >= 0; i--)
			cout << C[i];
	}
}

1.4.3乘法(单精度)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
using namespace std;
vector<int> mul(vector<int> A, int b) {
	vector<int> C;

	int t = 0;
	for (int i = 0; i < A.size()||t; i++) {
		if (i < A.size())			//保证无进位的答案才是正确答案
			t += A[i] * b;
		C.push_back(t % 10);
		t /= 10;				//此时的t为进位
	}
	while (C.size() > 1 && C.back() == 0)C.pop_back();			//去掉前导零
	return C;

}
int main() {
	string a;
	int b;
	cin >> a >> b;

	vector<int> A;
	for (int i = a.size() - 1; i >= 0; i--)
		A.push_back(a[i] - '0');

	auto C = mul(A, b);
	for (int i = C.size() - 1; i >= 0; i--)
		printf("%d", C[i]);
}

1.4.4除法

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
// a除b  商是c,余数是r
vector<int> div(vector<int> A, int b,int &r) {
	vector<int> C;			//商

	for (int i = A.size() - 1; i >= 0; i--) {
		r = r * 10 + A[i];
		C.push_back(r / b);
		r %= b;
	}
	reverse(C.begin(), C.end());
	while (C.size() > 1 && C.back() == 0)
		C.pop_back();
	return C;
}
int main() {
	string a;
	int b;
	cin >> a >> b;
	vector<int> A;
	
	for (int i = a.size() - 1; i >= 0; i--)
		A.push_back(a[i] - '0');

	int r = 0;
	auto C = div(A, b, r);
	for (int i = C.size() - 1; i >= 0; i--)
		cout << C[i];
	cout << endl << r;
}

1.5区间合并

  • 将有交集的区间合并,即求得并集

  • 假设只有端点相交也能合并 1,3 3,5 =1 ,5

    步骤:

    1.按照左端点排序

    2.扫描,合并

    #define _CRT_SECURE_NO_WARNINGS 1
    #include<iostream>
    #include<algorithm>
    #include<vector>
    using namespace std;
    typedef pair<int, int> PII;
    const int N = 100010;
    int n;
    vector<PII> segs;
    void merge(vector<PII>& segs) {
    	vector<PII> res;
    
    	sort(segs.begin(), segs.end());
    
    	int st = -2e9, ed = -2e9;
    	for(auto seg:segs)
    		if (ed < seg.first) {	//无交集
    			if (st != -2e9)
    				res.push_back({ st,ed });
    			st = seg.first, ed = seg.second;
    		}
    		else ed = max(ed, seg.second);
    	if (st != -2e9)   //判断最后一个区间与上一个区间无交集的情况,需要把最后一个区间加入结果集
    		res.push_back({ st,ed });
    	segs = res;    
    }
    int main() {
    	cin >> n;
    
    	for (int i = 0; i < n; i++) {
    		int l, r;
    		cin >> l >> r;
    		segs.push_back({ l,r });
    	}
    	merge(segs);
    	cout << segs.size() << endl;
    }
    

1.6双指针

核心思想:将两重for循环优化到O(n),都满足单调性
for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
-----先找单调关系
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
例子:输入一个字符串,输出空格隔开的单词   abc efg 输出efg和efg
 -i,j都是单调增的,不会减少
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
	char str[1000];
	gets_s(str);
	int n = strlen(str);
	for (int i = 0; i < n; i++)
	{
		int j = i;
		while (j < n && str[j] != ' ')j++;
		
		//这道题的具体逻辑
		for (int k = i; k < j; k++)cout << str[k];
		cout << endl;
		i = j;
	}
}
最大不重复的连续子序列
朴素算法:O(n^2)
for(int i=0;i<n;i++)
{
    for(int j=0;j<=i;j++)
    {
        if(check(i,j))
        {
            res=max(res,i-j+1);
        }
	}
}
双指针:保证每次j不是从0开始,因为每次i向后移动时,j只能不动或者向后移动,j表示以i为右边界时最左边的不重复数字下标
--i,j都是单调增的,不会减少
for(int i=0,j=0;i<n;i++)
{
    while(j<=i&&check(j,i))j++;
    res=max(res,i-j+1);
}
check:动态记录i,j区间次数

主要代码 
int s[N];
for(int i=0,j=0;i<n;i++)
{
	s[a[i]]++;
    while(s[a[i]]>1)//有重复且为a[i]
    {
    	  s[a[j]]--;//开始消去原来的操作,还原数组
        	j++;
	}
    res=max(res,i-j+1);
}

1.7位运算

常见操作

  • n的二进制表示中第k位数字是几 ,最后边是0位
haha

1.先把第k位移动到最后一位即0位:n>>k;

2.看个位是什么:x&1

3.合并: n>>k&1;

  • lowbit:也是树状数组的基本操作 作用:返回x的最后一位1是多少

    x=1010;lowbit(x)=10

    x=101000 ;lowbit(x)=1000

    实质操作:x&-x=x&(取反(x)+1)

    x=1010 … 10000000

    取反x=0101…0111111

    (取反x)+1=0101…1000000

应用:统计二进制x中1的个数

#include<iostream>
using namespace std;
int n;
int a[1000];
int lowbit(int n)
{
	return n & -n;
}
int main()
{
	scanf("%d", &n);
	int x;
	while (n--)
	{
		cin >> x; int res = 0;
		while (x)x -= lowbit(x), res++;//每次减去最后一个1
		cout << res << endl;
	}
}

1.8离散化

  • 整数离散化(保序的离散化(离散后也有序) 因为a数组是有序的)

  • 使用情况:值域比较大10e9 但个数比较小如1e5

  • 将值域分段映射到0到n-1的自然数中

两个问题:1.可能有重复的数字,需要去重

​ 2.如何算出离散化后的值

img

步骤
    1.先录入点,并将点插入vector中
    2.排序+去重
    3.开始映射这些点(用find函数),并进行下一步操作

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n
}

二.数据结构

2.1并查集

作用
1.将两个集合合并
2.询问两个元素是否在一个集合中
时间复杂度基本上是O(1);

基本原理
1.每一个集合用树来维护,每一个集合的编号是根节点,查找是去看他爸爸是不是根节点,不是再向上,所以每一个节点都要存储他的父节点,p[x]就是x的父节点
2.判断根节点 if(p[x]==x);
3.集合合并,直接让其中一个变成儿子(子节点),px是x的编号,py是y的编号,插入p[x]=y;
4.求x的集合编号,while(p[x]!=x)x=p[x],因为他的循环次数取决于树的高度,时间复杂度会很高,所以将其优化为:把所有子节点都指向根节点:(路径压缩);

模板
(1)朴素并查集:
int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);

(2)维护size的并查集:
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    size[b] += size[a];

(3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[I] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

2.2kmp

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];//回退一下
        // 匹配成功后的逻辑
    }
}

2.3栈和队列

  • 栈:先进后出
// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if (tt > 0)
{
	不空
}
  • 队列:先进先出
// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

//队尾的值
q[tt];

// 判断队列是否为空
if (hh <= tt)
{
	不空
}
  • 循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt)
{
	不空
}
  • 单调栈:找出每个数左边离它最近的比它大/小的数(右边也行)
  • 和双指针类似
-------左边第一个小于他的数--------
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}   



#include<iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt = 0;//tt=0表空,从1开始存
int main()
{
	scanf("%d", &n);
	for (int i = 0; i < n; i++)
	{
		int x; cin >> x;
		while (tt && stk[tt] >= x)tt--;
		if (tt)cout << stk[tt] << endl;
		else cout << -1 << endl;
		stk[++tt] = x;
	}

}
  • 单调队列:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;//0开始
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}

3.4堆:一颗二叉树(完全二叉树)

堆的三个基本功能:

  • 1.插入一个数

  • 2.求集合中的最小值

  • 3.删除最小值

  • 删除任意一个元素(STL直接实现不了) down和up同时写,但是只能执行一个

  • 修改任意一个元素 (STL直接实现不了)

  • image-20210902120100393
也是堆排序
以小根堆为例 
 带ph,hp的很不常用,但在dijstral中要用
存储时从1开始(数组存储)   x的左孩子:2*x 右孩子2*x+1
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置(完成4,5操作需要)
// hp[k]存储堆中下标是k的点是第几个插入的(完成4,5操作需要)
int h[N], ph[N], hp[N], size;

// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}
void down(int u)//向下调整,u是下标
{
    int t = u;//父与子中最小值的下标
    if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;   
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);//递归***很重要
    }
}

void up(int u)//向上调整,u是下标
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);//从n/2 开始是因为最下面一层没有儿子,故不需要down
//也可以理解为将所有的根节点down  因为 左 u*2  右  u*2+1  其根都是u  即 n/2


--------------------上面五个操作
#include<iostream>
#include<string.h>
using namespace std;
const int N = 1e5 + 10;
int h[N], s, n;
int ph[N];//第k个插入的数在堆中的下标
int hp[N];//堆中下标为k是第几个插入的数

void heap_swap(int a, int b)
{
    swap(ph[hp[a]], ph[hp[b]]);//先换外面指向堆的
    swap(hp[a], hp[b]);//在交换堆指向外面的
    swap(h[a], h[b]);//交换值
}
void down(int x)
{
    int t = x;
    if (2 * x <= s && h[2 * x] < h[t])t = 2 * x;
    if (2 * x + 1 <= s && h[2 * x + 1] < h[t])t = 2 * x + 1;
    if (t != x)
    {
        heap_swap(t, x);
        down(t);
    }
}
void up(int x)
{
    while (x / 2 && h[x] < h[x / 2])
    {
        heap_swap(x, x / 2);
        x /= 2;
    }
}
int main()
{
    scanf("%d", &n);
    char op[10];
    int k = 0;//第几个插入的数
    while (n--)
    {
        scanf("%s", op);
        if (!strcmp(op, "I"))
        {
            int x; scanf("%d", &x);
            h[++s] = x;
            ph[++k] = s;
            hp[s] = k;
            up(s);
        }
        else if (!strcmp(op, "PM"))printf("%d\n", h[1]);
        else if (!strcmp(op, "DM"))//删除第一个
        {
            heap_swap(1, s);
            s--; down(1);
        }
        else if (!strcmp(op, "D"))//删除第x个插入的数
        {
            int x; cin >> x;//第x插入的数
            int t = ph[x];// 这里一定要用t=ph[x]保存第k个插入点的下标
            heap_swap(t, s);// 因为在此处heap_swap操作后ph[k]的值已经发生
            s--;
            down(t);
            up(t);
        }
        else
        {
            int a, b;//第a个插入的数变为b
            scanf("%d%d", &a, &b);
            h[ph[a]] = b;
            down(ph[a]);
            up(ph[a]);
        }
    }
    return 0;
}

三.线段树

3.1基本操作

​ 线段树的目的:维护区间

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

除了最后一层之外都是满二叉树,可以用堆的方式来存:用一维数组存整棵树

编号是x: 父节点: [x/2] x>>1

​ 左儿子: 2x x<<1

​ 右儿子: 2x+1 x<<1|1

push up(u)

​ 父节点信息算子节点信息

build()

​ 将一段区间初始化成线段树

modify()

修改:

1.单点修改

​ u节点开始递归查找,将编号为x的节点的值修改为v

​ 若找到则修改,否则分别递归搜索左右子树

​ 最后调用pushup函数更新线段树

2.区间修改

​ 需要使用懒标记:增加一个标记,证明此区间修改,但此区间的子节点暂时不修改,等使用时再具体进行修改操作,这样可以有效降低时间复杂度。

:懒标记必须适用于整个区间,若不适用于整个区间,则需要分裂操作,将懒标记传入子节点,知道可以适用于一个区间。

query()

查询:

若树中结点已经被完全包含在[l,r]中,则返回;

否则看和左右子树是否有交集,并取返回值的最大值或最小值(根据题意来定)

push down

懒标记的向下传递

懒标记的含义为:该节点曾经被修改,但子节点尚未被更新

回溯时使用pushup 裂开传懒标记时则需要使用pushdown

例:最大数

#define _CRT_SECURE_NO_WARNINGS 
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

typedef long long LL;

const int N = 200010;
int m, p;
struct node
{
	int l, r;
	int v;			//区间[l,r]中的最大值
}tr[N * 4];

void pushup(int u) {			//由子节点的信息来计算父节点信息
	tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}

void build(int u, int l, int r) {
	tr[u] = { l,r };
	if (l == r) return;
	int mid = l + r >> 1;
	build(u << 1, l, mid);
	build(u << 1 | 1, mid + 1, r);
}

int query(int u, int l, int r) {
	if (tr[u].l >= l && tr[u].r <= r)
		return tr[u].v;				//树中节点,已经被完全包含在[l,r]中了

	int mid = tr[u].l + tr[u].r >> 1;
	int v = 0;
	if (l <= mid) v = query(u << 1, l, r);					//看跟左右是否有交集
	if (r > mid) v = max(v, query(u << 1 | 1, l, r));

	return v;
}

void modify(int u, int x, int v) {	//从u节点开始递归查找,将编号为x的节点的值修改为v
	if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
	else {
		int mid = tr[u].l + tr[u].r >> 1;
		if (x <= mid) modify(u << 1, x, v);
		else modify(u << 1 | 1, x, v);
		pushup(u);
	}
}

int main() {
	int n = 0, last = 0;
	scanf("%d%d", &m, &p);
	build(1, 1, m);

	int x;
	char op[2];
	while (m--)
	{
		scanf("%s%d", op, &x);
		if (*op == 'Q') {
			last = query(1, n - x + 1, n);
			printf("%d\n", last);
		}
		else {
			modify(1, n + 1, ((LL)last + x) % p);
			n++;
		}
	}
}

例2:区间最大公约数

该题为了将区间修改转化为单点修改,线段树维护的应是差分序列。

另外可以推出:一个区间内的最大公约数等于其差分数组的最大公约数

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 500010;

int n, m;
LL w[N];
struct Node         //用线段树维护差分序列
{
    int l, r;
    LL sum, d;          //区间和    最大公约数
}tr[N * 4];

LL gcd(LL a, LL b)
{
    return b ? gcd(b, a % b) : a;
}

void pushup(Node& u, Node& l, Node& r)
{
    u.sum = l.sum + r.sum;
    u.d = gcd(l.d, r.d);
}

void pushup(int u)
{
    pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}

void build(int u, int l, int r)
{
    if (l == r)
    {
        LL b = w[r] - w[r - 1];
        tr[u] = { l, r, b, b };
    }
    else
    {
        tr[u].l = l, tr[u].r = r;
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}

void modify(int u, int x, LL v)
{
    if (tr[u].l == x && tr[u].r == x)
    {
        LL b = tr[u].sum + v;
        tr[u] = { x, x, b, b };
    }
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

Node query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u];
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (r <= mid) return query(u << 1, l, r);
        else if (l > mid) return query(u << 1 | 1, l, r);
        else
        {
            auto left = query(u << 1, l, r);
            auto right = query(u << 1 | 1, l, r);
            Node res;
            pushup(res, left, right);
            return res;
        }
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%lld", &w[i]);
    build(1, 1, n);

    int l, r;
    LL d;
    char op[2];
    while (m--)
    {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'Q')
        {
            auto left = query(1, 1, l);
            Node right({ 0, 0, 0, 0 });
            if (l + 1 <= r) right = query(1, l + 1, r);
            printf("%lld\n", abs(gcd(left.sum, right.d)));
        }
        else
        {
            scanf("%lld", &d);
            modify(1, l, d);
            if (r + 1 <= n) modify(1, r + 1, -d);
        }
    }

    return 0;
}

四.排序

4.1快速排序

  • 1.用一个数字来分界(随机)
  • 2.先操作
  • 3.在递归
从1开始到n
void QS( int l, int r)
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = a[l + r >> 1];
    while (i < j)
    {
        do i++; while (a[i] < x);
        do j--; while (a[j] > x);
        if (i < j) swap(a[i], a[j]);
    }
    QS(l, j), QS(j + 1, r);
}

4.2归并排序

  • 归并排序(nlog2n)merge_sort
  • 1.中间分,分为左右mid=(l+r)/2;
  • 2.先递归排序左边和右边
  • 3.归并————合二为一***
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int temp[N];
void merge_sort(int l, int r)
{
    if (l >= r) return;
    int mid = l + r >> 1;
    merge_sort(l, mid), merge_sort(mid + 1, r);
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j])temp[++k] = q[i++];
        else temp[++k] = q[j++];
    }
    while (i <= mid)temp[++k] = q[i++];
    while (j <= r)temp[++k] = q[j++];
    //还回去
    for (i = l, j = 1; i <= r; j++, i++)q[i] = temp[j];
}
int main()
{
    int n; scanf("%d", &n);
    for (int i = 1; i <= n; i++)scanf("%d", &q[i]);
    merge_sort(1, n);
    for (int i = 1; i <= n; i++)printf("%d ", q[i]);
}

4.3冒泡排序

void BubbleSort(int len)
{
	int i, j, temp;
	
	for (i = 1; i <= len - 1; i++)
	{
		int flags = 0;
		for (j = 1; j <= len - i; j++)
		{
			if (a[j] > a[j + 1])
			{
				temp = a[j];
				a[j] = a[j + 1];
				a[j + 1] = temp;
				flags = 1;//无序,flags设置为1
			}
		}
		if (flags == 0)
			return;
	}
}

五.图论

5.1树和图的存储和遍历

(1) 邻接矩阵:g[a][b] 存储边a->b
(2) 邻接表:
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;
// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 初始化
idx = 0;
memset(h, -1, sizeof h);

5.2最短路

5.2.1朴素Dijkstra

稠密图(邻接矩阵)边多点少

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
		if(t==n)break;
        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

权值为正时自环那不可能出现在最短路中
 scanf("%d%d", &n, &m);

    memset(g, 0x3f, sizeof g);//无边的设置为正无穷
    
    for (int i = 1; i <= m; i ++ )
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b]=min(g[a][b],c);//含有重边,保存最小的就行
    }

5.2.2堆优化Dijkstra

//也能去除自环,重边等情况
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 2e5;
int n, m;
typedef pair<int, int> PII;
int h[N], e[N], ne[N], w[N], idx;
bool st[N];//最短距离是否确定
int d[N];
priority_queue < PII, vector<PII>, greater<PII>>q;
void add(int a, int b, int c)
{
    e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx++;
}
int dijkstra(int x)  // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
{
    memset(d, 0x3f, sizeof d);
    q.push({ 0,x });//距离在前,默认按距离排序
    d[x] = 0;
    while (!q.empty())
    {
        auto t = q.top(); q.pop();

        int ver = t.second, distance = t.first;
        if (st[ver])continue;
        st[ver] = true;
        for (int i = h[ver]; i != -1; i = ne[i])//遍历ver所有邻接的点
        {
            int j = e[i];
            if (d[j] > distance + w[i])//i只是个下标,e中在存的是i这个下标对应的点。
            {
                d[j] = distance + w[i];
                q.push({ d[j], j });
            }
        }
    }

    if (d[n] == 0x3f3f3f3f)return -1;
    return d[n];
}
int main()
{
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    while (m--)
    {
        int a, b, c; scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }
    cout << dijkstra(1) << endl;
}

5.2.3Bellman—ford

时间复杂度:n*m

有边数限制时用,可以有负环

存边的方式可以随便存只要能遍历所有边,这里用结构体
如果有负权回路,就不一定有最短距离
这个方法可以求带负权回路的最短距离问题
Bellmax_ford可以求是否存在负权回路
    int n, m;       // n表示点数,m表示边数
int d[N];        // dist[x]存储1到x的最短路距离
int backup[N];

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edge[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(d, 0x3f, sizeof d);
    d[1] = 0;

    // 如果第n次迭代仍然会松弛(更新)三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    //n就是一个限制,限制不超过n条边到n点
    for (int i = 0; i < n; i ++ )//第k次迭代,结束后表示不超过k条边,走到每个点的最短距离
    {
        memcpy(backup, d, sizeof(d));//存储上一次更新后的值,防止串联反应,一般的最短路问题中不需要备份距离数组,只有当有边数限制时才需要。
        for (int j = 0; j < m; j ++ )
        {
            int a = edge[j].a, b = edge[j].b, w = edge[j].w;
           d[b] = min(d[b], backup[a] + w);
        }
    }
    
表示无法到达,这里不能返回-1,有可能最短距离正好是-1
    if (dist[n] > 0x3f3f3f3f / 2) return -0x3f3f3f3f;
    return dist[n];
}

5.2.4spfa算法

  • 队列优化的Bellman-Ford算法
  • 宽搜做优化
  • 可以解决dijkstra,一般还比他快,如果被卡了,用堆优化dijkstra
  • 必须无负环,只要无负环就可以用spfa,大部分就是无负环的情况
只有back[a]也就是d[a]改变,d[b]才改变
队列里存变小的d[a];
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

5.3最小生成树

5.3.1.prim算法

稠密图

int n;      // n表示点数
int g[N][N];        // 邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];     // 存储每个点是否已经在生成树中


// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

5.3.2krustral算法

稀疏图

int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组

struct Edge     // 存储边
{
    int a, b, w;

    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M];

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并
        {
            p[a] = b;
            res += w;
            cnt ++ ;//边数
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

六.数论

6.1快速幂

  • 利用初中数学知识

  • 先编程程计算a的b次方

  • 在算取余会更好想

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

求 m^k mod p,时间复杂度 O(logk)。
//类型转换防止溢出
快速幂算法的一个问题:当模数p>1e9的时候,在相乘的时候可能爆long long了 ,龟速幂解决
一般a,k,p都是小于int范围
int qmi(int a, int k, int p)
{
	int res = 1;
    a%=p;
	while (k)
	{
		if (k & 1)res = (LL)res *a % p;
		a = (LL)a * a % p;
		k >>= 1;
	}
	return res;
}
龟速幂
LL qmul(LL a,LL b, LL p)//计算a*b%p,和快速幂类似
{
	LL ans = 0;
	while (b)
	{
		if (b & 1)ans += a, ans %= p;
		a = (a + a) % p;
		b >>= 1;
	}
	return ans;
}
LL qmi(LL a, LL b, LL p)
{
	LL ans = 1;
	while (b)
	{
		if (b & 1)ans = qmul(ans, a, p) % p;
		b >>= 1;
		a = qmul(a, a, p) % p;
	}
	return ans;
}

6.2欧几里得算法

求两个正整数的最大公约数,时间复杂度 O(logn)

int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}

6.3扩展欧几里得算法

裴蜀定理:若 a,b 是整数,且 (a,b)=d,那么对于任意的整数 x,y, ax+by 都一定是 d 的倍数,特别地,一定存在整数 x,y,使 ax+by=d成立。

扩展欧几里得算法可以在 O(logn) 的时间复杂度内求出系数 x,y。

int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1; y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= (a/b) * x;
    return d;
}

6.4试除法求约数

试除法:求出约数;判断约数 分解质因数都可以用试除法

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )//i*i<=n容易爆int
        if (x % i == 0)
            return false;
    return true;
}

6.5线性筛素数

可以在 O(n)O(n) 的时间复杂度内求出 1∼n1∼n 之间的所有质数

int primes[N], cnt;
bool st[N];

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;
        }
    }
}

6.6欧拉函数

欧拉函数,一般记为 ϕ(n)ϕ(n),表示小于等于 nn 的数中与 nn 互质的数的个数。
如果 n=pa11×pa22×…×pammn=p1a1×p2a2×…×pmam,
则 ϕ(n)=n(1−1p1)…(1−1pm)ϕ(n)=n(1−1p1)…(1−1pm).

欧拉函数的常用性质:

如果 n,mn,m 互质,则 ϕ(nm)=ϕ(n)ϕ(m)ϕ(nm)=ϕ(n)ϕ(m);
小于等于 nn,且与 nn 互质的数的和是 ϕ(n)×n/2ϕ(n)×n/2;
欧拉定理:如果 n,an,a 互质,且均为正整数,则 aϕ(n)≡1(mod n)aϕ(n)≡1(mod n);
下面的代码可以在 O(n)O(n) 的时间复杂度内求出 1∼n1∼n 中所有数的欧拉函数:

int primes[N], euler[N], cnt;
bool st[N];

// 质数存在primes[]中,euler[i] 表示
// i的欧拉函数
void get_eulers(int n)
{
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])
        {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0)
            {
                euler[i * primes[j]] = euler[i] * primes[j];
                break;
            }
            euler[i * primes[j]] = euler[i] * (primes[j] - 1);
        }
    }
}

七.搜索

7.1BFS搜索

7.1.1Flood Fill

​ BFS两种问题:1.最短距离 2.最小步数

Flood Fill算法:

求连通块,比如将蓝色区域看作洼地,可以用洪水填满

例:池塘计数:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
const int N = 1010,M=N*N;
typedef pair<int, int> PII;

int n, m;
char g[N][N];		//地图存储
PII q[M];			//队列
bool st[N][N];		//判重数组

void bfs(int sx, int sy) {		//队列是模拟队列
	int hh = 0, tt = 0;
	q[0] = { sx,sy };
	st[sx][sy] = true;

	while (hh<=tt)
	{
		PII t = q[hh++];

		for(int i=t.first-1;i<=t.first+1;i++)
			for (int j = t.second - 1; j <= t.second + 1; j++) {
				if (i == t.first && j == t.second) continue;
				if (i < 0 || i >= n || j < 0 || j >= m) continue;
				if (g[i][j] == '.' || st[i][j]) continue;

				st[i][j] = true;
				q[++tt] = { i,j };
			}
	}
}
int main() {
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		cin >> g[i];

	int cnt = 0;
	for(int i=0;i<n;i++)
		for(int j=0;j<m;j++)
			if (g[i][j] == 'W' && !st[i][j]) {
				bfs(i, j);
				cnt++;
			}
	cout << cnt;
}

flood fill在实际应用中可能是一个题目中的一小部分

7.1.2最短路模型

使用的是BFS里的性质:当所有边权重相等时,使用BFS可以直接得到单源最短路(线性的时间复杂度)

第一次搜到一定是最短

例:迷宫问题

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<algorithm>
#include<cstring>

#define x first
#define y second
using namespace std;

typedef pair<int, int> PII;

const int N = 1010, M = N * N;
int n;
int g[N][N];
PII q[M];		 //队列
PII pre[N][N];	//上一次的路径,替换的是状态值

void bfs(int sx, int sy) {
	
	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
	
	int hh = 0, tt = 0;
	q[0] = { sx,sy };

	memset(pre, -1, sizeof pre);
	pre[sx][sy] = { 0,0 };
	while (hh <= tt) {
		PII t = q[hh++];

		for (int i = 0; i < 4; i++) {
			int a = t.x + dx[i], b = t.y + dy[i];
			if (a < 0 || a >= n || b < 0 || b >= n) continue;	//出界
			if (g[a][b]) continue;	// 墙
			if (pre[a][b].x != -1)	continue;		//被遍历过

				q[++tt] = { a,b };
				pre[a][b] = t;
		}
	}
}

int main(){
	scanf("%d", &n);

	for(int i=0;i<n;i++)
		for (int j = 0; j < n; j++) {
			scanf("%d", &g[i][j]);
		}

	bfs(n - 1, n - 1);			//终点往起点找,不需要缓存数组

	PII end(0, 0);

	while (true)
	{
		cout << end.x << " " << end.y << endl;
		if (end.x == n - 1 && end.y == n - 1) break;
		end = pre[end.x][end.y];
	}
	
}

为了避免再使用一个缓冲数组,最短路可以倒推,由终点推起点

7.1.3多源BFS

BFS在边权相等时可求最短路的证明:

队列性质:

1.两段性 最多有两段

2.单调性 (数学归纳法证明)

​ 2.1 初始是 0

​ 2.2 假设当前满足

反证法,根据单调性可证

例:矩阵距离:

将所有的源加入队列成为起点,以源作BFS,第一次BFS得出的结论就是最优解

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>

#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 1010, M = N * N;

int n, m;
char g[N][N];
PII q[M];
int dist[N][N];

void bfs() {

	memset(dist, -1, sizeof dist);
	int hh = 0, tt = -1;

	for(int i=0;i<n;i++)
		for(int j=0;j<m;j++)
			if (g[i][j] == '1') {

				dist[i][j] = 0;
				q[++tt] = { i,j };
			}
	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };

	while (hh<=tt)
	{
		PII t = q[hh++];
		for (int i = 0; i < 4; i++) {
			int a = t.x + dx[i], b = t.y + dy[i];
			if (a < 0 || a >= n || b < 0 || b >= m)continue;
			if (dist[a][b] != -1)continue;

			dist[a][b] = dist[t.x][t.y] + 1;
			q[++tt] = { a,b };
		}
	}
}
int main() {
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		cin >> g[i];

	bfs();

	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++)
			cout << dist[i][j] << " ";
		cout << endl;
	}
}


7.1.4最小步数模型

本质上还是BFS,求达到状态的最小步数,如果需要输出路径,则需要开一个pre数组记录上一步的操作

例:魔板:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
#include<queue>
using namespace std;

char g[2][4];

unordered_map<string, int> dist;			//存储每一个状态的步数
unordered_map<string, pair<char, string>>	pre; //存储这个状态是从哪一个状态转移过来的,并且操作是a、b或c
queue<string> q;

void set(string state) {
	for (int i = 0; i < 4; i++)
		g[0][i] = state[i];
	for (int i = 3, j = 4; i >= 0; i--, j++)
		g[1][i] = state[j];

}
string get() {
	string res;
	for (int i = 0; i < 4; i++)
		res += g[0][i];
	for (int i = 3; i >= 0; i--)
		res += g[1][i];
	return res;
}
string move0(string state) {		//交换上下两行
	set(state);
	for (int i = 0; i < 4; i++) {
		swap(g[0][i], g[1][i]);
	}

	return get();
}
string move1(string state) {				//最右边一列插入到最左边
	set(state);
	char v0 = g[0][3], v1 = g[1][3];
	for (int i = 3; i > 0; i--)
		for (int j = 0; j < 2; j++)
			g[j][i] = g[j][i - 1];
	g[0][0] = v0, g[1][0] = v1;
	return get();
}
string move2(string state) {
	set(state);
	char v = g[0][1];
	g[0][1] = g[1][1];
	g[1][1] = g[1][2];
	g[1][2] = g[0][2];
	g[0][2] = v;
	return get();
}

void bfs(string start,string end) {
	if (start == end)
		return;
	q.push(start);
	dist[start] = 0;

	while (q.size())
	{
		auto t = q.front();
		q.pop();

		string M[3];
		M[0] = move0(t);
		M[1] = move1(t);
		M[2] = move2(t);

		for (int i = 0; i < 3; i++) {
			string m = M[i];
			if (dist.count(m) == 0) {
				dist[m] = dist[t] + 1;
				pre[m] = { char(i + 'A'), t };
				if (m == end)
					break;
				q.push(m);
			}
			
		}
	}
}

int main() {
	int x;
	string start, end;
	for (int i = 0; i < 8; i++) {
		cin >> x;
		end += char(x + '0');
		
	}
	
	for (int i = 0; i < 8; i++)
		start += char(i + '1');		//将数字转化为ASCII码

	bfs(start, end);

	cout << dist[end] << endl;

	string res;
	while (end != start) {
		res += pre[end].first;
		end = pre[end].second;
	}
	reverse(res.begin(), res.end());

	if (res.size())
		cout << res << endl;
}

把每一个状态转化为字符串当作key

7.1.5双端队列BFS

双端队列主要解决图中边的权值只有0或者1的最短路问题

操作:
每次从队头取出元素,并进行拓展其他元素时

1、若拓展某一元素的边权是0,则将该元素插入到队头
2、若拓展某一元素的边权是1,则将该元素插入到队尾

原理:BFS队列具有两段性,所以可以求边权为0、1的最短路问题;BFS队列同时具有单调性,所以边权为0的边入队时进入队头,为1的边进入队尾;与堆优化Dijkstra 一样,必须在出队时才知道每个点最终的最小值

例:电路维修:

题目中需要注意路径和点的边权怎样确定和转换

踩过格子到达想去的点时,需要判断是否需要旋转电线,若旋转电线表示从 当前点 到 想去的点 的边权是1,若不旋转电线则边权是0

按左上角,右上角,右下角,左下角遍历的顺序

1、dx[]和dy[]表示可以去其他点的方向
2、id[]和iy[]表示需要踩某个方向的各种才能去到相应的点
3、cs[]表示当前点走到4个方向的点理想状态下格子形状(边权是0的状态)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
#include<deque>			//双端队列
using namespace std;
typedef pair<int, int> PII;
#define x first
#define y second

const int N = 510, M = N * N;

int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];

int bfs() {
	deque<PII> q;
	memset(st, 0, sizeof st);
	memset(dist, 0x3f, sizeof dist);
	
		//1、dx[]和dy[]表示可以去其他点的方向
		//2、id[]和iy[]表示需要踩某个方向的各种才能去到相应的点
		//3、cs[]表示当前点走到4个方向的点理想状态下格子形状(边权是0的状态)
	char cs[5]="\\/\\/";				//电路方向映射
	int dx[4] = { -1,-1,1,1 }, dy[4] = { -1,1,1,-1 };			//到达点的方向
	int ix[4] = { -1,-1,0,0 }, iy[4] = { -1,0,0,-1 };			//需要踩过的格子的方向

	dist[0][0] = 0;
	q.push_back({ 0,0 });

	while (q.size())
	{
		auto t = q.front();
		q.pop_front();

		int x = t.x, y = t.y;

		if (x == n && y == m)
			return dist[x][y];

		if (st[t.x][t.y]) continue;
		st[x][y] = true;

		for (int i = 0; i < 4; i++) {
			int a = x + dx[i], b = y + dy[i];
			if (a<0 || a>n || b<0 || b>m) continue;		//越界
			int ga = x + ix[i], gb = y + iy[i];			//在g数组里的下标
			int w = (g[ga][gb] != cs[i]);				//踩过格子到达想去的点时,需要判断是否需要旋转电线,若旋转电线表示从 当前点 到 想去的点 的边权是1,若不旋转电线则边权是0
			int d = dist[x][y] + w;
			if (d <= dist[a][b]) {
				dist[a][b] = d;					//更新dist
				if (!w) q.push_front({ a,b });		//插到队头
				else q.push_back({ a,b });
			}
		}
	}
	return -1;		//一定不会被执行
}
int main() {
	int T;
	cin >> T;
	while (T--) {
		cin >> n >> m;
		for (int i = 0; i < n; i++)
			cin >> g[i];

		if (n + m & 1)
			puts("NO SOLUTION");
		else printf("%d\n", bfs());

	}

	return 0;
}

7.1.6.双向广搜BFS

单向广搜:从起点到终点

双向广搜,以起点到终点、终点到起点两个方向进行搜索

一般用在最小步数里

双向广搜 BFS
双向:简而言之就是从起点(正向搜索)和终点(逆向搜索)同时开始搜索,当两个搜索产生的一个子状态相同时就结束搜索。

通常有两种实现方法:

1、用一个队列来储存子状态,起点和终点先后入队,正向搜索和逆向搜索交替进行,两个方向的搜索交替扩展子状态。直到两个方向的搜索产生相同的子状态结束。

2、两个方向的搜索虽然是交替扩展子状态的。但是两个方向生成的子状态的速度不一定平衡。所以,可以每次选择子状态数较少的那个方向先进行扩展。这样就不会出现两个方向生成子状态的速度的不平衡

例:字串变换:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
#include<queue>
using namespace std;
const int N = 7;
int n;
string a[N], b[N];

int extend(queue<string>& q, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[], string b[]) {
  
        auto t = q.front();             //取出状态
        q.pop();

        for (int i = 0; i < t.size(); i++)
            for (int j = 0; j < n; j++) {
                if (t.substr(i, a[j].size()) == a[j]) {     //扩展
                    string r = t.substr(0, i) + b[j] + t.substr(i + a[j].size());
                    if (db.count(r))     return da[t] + db[r] + 1;          //对方队列有r状态,则求出结果
                    if (da.count(r))continue;                               //自己队列有r状态则说明走重了,跳过

                    da[r] = da[t] + 1;                                      //将状态放入d数组和队列里
                    q.push(r);
                }
            }
    return 11;
}
int bfs(string A, string B) {
    if (A == B)
        return 0;
    queue<string> qa, qb;           //双向广搜的队列
    unordered_map<string, int> da, db;
    //分别入队
    qa.push(A), da[A] = 0;
    qb.push(B), db[B] = 0;

    while (qa.size() && qb.size()) {
        int t;
        if (qa.size() < qb.size())
            t = extend(qa, da, db, a, b);       //一次只向下扩展一层
        else
            t = extend(qb, db, da, b, a);

        if (t <= 10)
            return t;
    }
    return -1;
}

int main() {
    string A, B;
    cin >> A >> B;
    // 读入扩展规则,分别存在a数组和b数组
    while (cin >> a[n] >> b[n]) n++;
    int step = bfs(A, B);
    if (step == -1) puts("NO ANSWER!");
    else cout << step << endl;
}

7.1.7.A*算法

A*算法边权是非负就可以,不一定都为1

必须保证有解

1.把BFS中的队列换成优先队列,存从起点到当前点的真实距离,到从当前点到终点的预测距离

2.选择预测距离最小的点扩展

3.算法结束:当终点第一次出队的时候break(第一次出队就是最短距离的性质只对终点成立)

核心估价函数的界定

例:八数码

最小步数的具体路径需要用一个pre存储,把每一个状态看成一个整体的字符串来处理

#define _CRT_SECURE_NO_WARNINGS 1
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#include<queue>

#define x first
#define y second
using namespace std;

typedef pair<int, string> PIS;

int f(string state) {			//估价函数
	int res = 0;
	for(int i=0;i<state.size();i++)
		if (state[i] != 'x') {				//求曼哈顿距离
			int t = state[i] - '1';
			res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);		//真实的横纵坐标

		}
	return res;
}

string bfs(string start) {
	string end = "12345678x";

	unordered_map<string, int> dist;	//距离,真实值
	unordered_map<string, pair<char, string>> prev;			//存储上一步操作,分别存储状态、上一步的操作和状态
	priority_queue<PIS, vector<PIS>, greater<PIS>> heap;
	char op[] = "urdl";
	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
	
	
	dist[start] = 0;		
	heap.push({ f(start),start });

	while (heap.size()) {
		auto t = heap.top();
		heap.pop();

		string state = t.y;
		if (state == end) break;	//到达终点

		int x, y;				//'x'在矩阵中的真实坐标
		for(int i=0;i<9;i++)
			if (state[i] == 'x') {
				x = i / 3, y = i % 3;
				break;
			}

		string source = state;
		for (int i = 0; i < 4; i++) {
			int a = x + dx[i], b = y + dy[i];
			if (a < 0 || a >= 3 || b < 0 || b >= 3) continue;
			state = source;								//source是交换前的状态,交换后的状态存在state中
			swap(state[x * 3 + y], state[a * 3 + b]);	//交换元素
			if (!dist.count(state) || dist[state] > dist[source] + 1) {	//之前没有被扩展过或者之前被扩展过但距离比较大
				dist[state] = dist[source] + 1;
				prev[state] = { op[i],source };
				heap.push({ f(state) + dist[state], state });
			}
		}

	}
	string res;
	while (end != start) {
		res += prev[end].x;
		end = prev[end].y;
	}
	reverse(res.begin(), res.end());
	return res;
}

int main() {
	string start, seq;
	char c;

	for (int i = 0; i < 9; i++)
	{
		char c;
		cin >> c;
		start += c;
		if (c != 'x') seq += c;
	}

	int cnt = 0;
	for (int i = 0; i < 8; i++)
		for (int j = i; j < 8; j++) {			//逆序对数量
			if (seq[i] > seq[j])
				cnt++;
		}

	if (cnt & 1)					//奇数逆序对无解
		cout << "unsolvable";
	else cout << bfs(start) << endl;
}

例:第k短路

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

#define x first
#define y second

using namespace std;

typedef pair<int, int> PII;
typedef pair<int, PII> PIII;        
const int N = 1010, M = 200010;

int n, m, S, T, K;
int h[N], rh[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int h[], int a, int b, int c)
{
    e[idx] = b;
    w[idx] = c;
    ne[idx] = h[a];
    h[a] = idx++;
}

void dijkstra()
{
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({ 0,T });//终点,heap里存储的是距离和点位
    memset(dist, 0x3f, sizeof dist);
    dist[T] = 0;

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.y;
        if (st[ver]) continue;
        st[ver] = true;

        for (int i = rh[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                heap.push({ dist[j],j });
            }
        }
    }
}

int astar()
{
    priority_queue<PIII, vector<PIII>, greater<PIII>> heap;         //存储每个点的估价函数值加离起点的距离   和每个点的
    // 谁的d[u]+f[u]更小 谁先出队列
    heap.push({ dist[S], {0, S} });
    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();
        int ver = t.y.y, distance = t.y.x;
        cnt[ver]++;         //点位被访问过几次
        //如果终点已经被访问过k次了 则此时的ver就是终点T 返回答案

        if (cnt[T] == K) return distance;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            /*
            如果走到一个中间点都cnt[j]>=K,则说明j已经出队k次了,且astar()并没有return distance,
            说明从j出发找不到第k短路(让终点出队k次),
            即继续让j入队的话依然无解,
            那么就没必要让j继续入队了
            */
            if (cnt[j] < K)
            {
                // 按 真实值+估计值 = d[j]+f[j] = dist[S->t] + w[t->j] + dist[j->T] 堆排
                // 真实值 dist[S->t] = distance+w[i]
                heap.push({ distance + w[i] + dist[j],{distance + w[i],j} });
            }
        }
    }
    // 终点没有被访问k次
    return -1;
}

int main()
{
    cin >> m >> n;
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);
    for (int i = 0; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(h, a, b, c);
        add(rh, b, a, c);
    }
    cin >> S >> T >> K;
    // 起点==终点时 则d[S→S] = 0 这种情况就要舍去 ,总共第K大变为总共第K+1大 
    if (S == T) K++;
    // 从各点到终点的最短路距离 作为估计函数f[u]
    dijkstra();
    cout << astar();
    return 0;
}


7.2DFS搜索

7.2.1基于连通性的模型

​ 类似宽搜两大问题:1.Flood Fill 2.图与树的遍历

​ 还原现场的问题:一个状态可能需要多次使用时才需要恢复现场

​ 连通性问题一般来说既可以用DFS也可以用BFS

7.2.2搜索顺序

分内部搜索和外部搜索两种:如果把一个棋盘当作一个状态、内部搜索则需要回溯现场

例:单词接龙

内部搜索,自己的状态值会改变,所以需要恢复现场;此题的核心是建图:图的权值是每两个单词最小的重合距离

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 21;

int n;
string word[N];
int g[N][N];			//权值为两个单词重合部分长度的最小值
int used[N];		
int ans;

void dfs(string dragon,int last) {
	ans = max((int)dragon.size(), ans);

	used[last]++;

	for (int i = 0; i < n; i++)
		if (g[last][i] && used[i] < 2)
			dfs(dragon + word[i].substr(g[last][i]), i);
	
	used[last]--;
}

int main() {
	cin >> n;
	for (int i = 0; i < n; i++) cin >> word[i];
	char start;
	cin >> start;

	for(int i=0;i<n;i++)
		for (int j = 0; j < n; j++) {
			string a = word[i], b = word[j];
			for(int k=1;k<min(a.size(),b.size());k++)
				if (a.substr(a.size() - k, k) == b.substr(0, k)) {
					g[i][j]=k;
					break;
				}
		}
	for (int i = 0; i < n; i++)
		if (word[i][0] == start)
			dfs(word[i], i);

	cout << ans << endl;
	return 0;
}

7.2.3剪枝

几种剪枝方式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例:数独游戏

剪枝:1.优化搜索顺序,先搜索分支节点较少的点

​ 2.排除等效冗余

​ 3.位运算压缩状态:判断条件使用三个数组row col cell表示,数组中每一个元素存储的是一个二进制状态,代表这一行或者是列已经填了哪些数

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 9, M = 1 << N;        //1<< : 1向左移位且空位补零

//ones表示0-2^9里每个数有多少个1,map是二进制状态到十进制数的映射,比如map[1000]=4
int ones[M], map[M];               
int row[N], col[N], cell[3][3];
char str[100];

void init()
{
    for (int i = 0; i < N; i++)
        row[i] = col[i] = (1 << N) - 1;

    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            cell[i][j] = (1 << N) - 1;
}

//表示在xy上填上一个数还是把这个数删掉
void draw(int x, int y, int t, bool is_set)
{
    if ( is_set) str[x * N + y] = '1' + t;
    else str[x * N + y] = '.';

    int v = 1 << t;
    if (!is_set) v = -v;

    row[x] -= v;
    col[y] -= v;
    cell[x / 3][y / 3] -= v;
}

int lowbit(int x)
{
    return x & -x;
}

//求xy上能填哪些数
int get(int x, int y)
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}

bool dfs(int cnt)
{
    if (!cnt) return true;

    //优化搜索顺序,优先搜索可以填的数最少的状态
    int minv = 10;
    int x, y;
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            if (str[i * N + j] == '.')
            {
                int state = get(i, j);
                if (ones[state] < minv)
                {
                    minv = ones[state];
                    x = i, y = j;
                }
            }

    int state = get(x, y);
    for (int i = state; i; i -= lowbit(i))
    {
        int t = map[lowbit(i)];
        draw(x, y, t, true);
        if (dfs(cnt - 1)) return true;
        draw(x, y, t, false);
    }

    return false;
}

int main()
{
    for (int i = 0; i < N; i++) map[1 << i] = i;
    for (int i = 0; i < 1 << N; i++)
        for (int j = 0; j < N; j++)
            ones[i] += i >> j & 1;

    while (cin >> str, str[0] != 'e')
    {
        init();

        int cnt = 0;
        for (int i = 0, k = 0; i < N; i++)
            for (int j = 0; j < N; j++, k++)
                if (str[k] != '.')
                {
                    int t = str[k] - '1';
                    draw(i, j, t, true);
                }
                else cnt++;

        dfs(cnt);

        puts(str);
    }

    return 0;
}


例2:木棒:

​ 思路:枚举大木棍的长度,使用小木棍尝试能否组成大木棍,dfs函数参数为大木棍的数量;当前大木棍的长度;当前枚举到的小木棍

​ 剪枝:1.优化搜索顺序:从小到大枚举

​ 2.枚举大木棍长度时只枚举小木棍总和的约数

​ 3.一个小木棍在某个位置不合法后跳过与他长度相同的所有小木棍,排除等效庸余

​ 4.一个大木棍上第一个小木棍和最后一个小木棍搜不到合法解时证明该大木棍的长度不正确

//剪枝:1.优化搜索顺序,从小到大枚举
//2.排除等效冗余,按照组合数枚举 
//3.只枚举约数
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 70;

int n;
int w[N], sum, length;
bool st[N];		//木棍有没有被用过
		//当前枚举到的每根大棍   当前大棍的长度  开始的位置 
bool dfs(int u, int s, int start) {
	if (u * length == sum) return true;

	if (s == length) return dfs(u + 1, 0, 0);

	//在大棍里枚举小棍  剪枝: 从start开始枚举
	for (int i = start; i < n; i++) {
		if (st[i]) continue;
		if (s + w[i] > length) continue;		//可行性剪枝

		st[i] = true;
		if (dfs(u, s + w[i], i + 1))	return true;
		st[i] = false;

		//在恢复现场后的情况都是失败的情况
		//剪枝:搜索一遍后放入大木棍中的第一个木棍都不合法  说明这个长度不合法
		if (!s) return false;
		//剪枝:最后一个木棍不合法,说明这个长度不合法
		if (s + w[i] == length) return false;

		//剪枝  排除等效冗余
		int j = i;
		while (j < n && w[j] == w[i]) j++;
		i = j - 1;
	}

	return false; 
}
int main() {
	while (cin >> n, n) {
		
		memset(st, 0, sizeof st);
		sum = 0;

		for (int i = 0; i < n; i++) {
			cin >> w[i];
			sum += w[i];
		}

		//剪枝:优化搜索顺序
		sort(w, w + n);
		reverse(w, w + n);

		length = 1;
		while (true) {
			//剪枝1:枚举的大棍长度必须是小棍长度总和的约数
			if (sum % length == 0 && dfs(0, 0, 0)) {
				cout << length << endl;
				break;
			}
			length++;
		}
	}

	return 0;
}

7.2.4迭代加深

目的:限制迭代次数,防止陷入过深的搜索,一般用于树的深度很深但答案很浅的情况

例:加成序列

剪枝1:优先枚举较大的数

剪枝2:排除等效冗余

思路:先枚举要递归的深度,如果迭代次数超过递归深度直接返回失败

dfs函数的参数为:当前的层数;枚举的递归深度

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 110;

int n;
int path[N];
bool dfs(int u, int depth) {
	if (u > depth)
		return false;
	if (path[u - 1] == n) return true;	//找到了合法解

	bool st[N] = { 0 };
	for(int i=u-1;i>=0;i--)							//剪枝操作:优先枚举较大的数
		for (int j = i; j >= 0; j--) {
			int s = path[i] + path[j];
			if (s > n || s <= path[u - 1] || st[s])			//剪枝操作
				continue;

			st[s] = true;
			path[u] = s;
			if (dfs(u + 1, depth))			//这种写法是搜到答案后连锁反应,退出所有的递归
				return true;
		}
	return false;
}
int main() {
	path[0] = 1;
	while (cin>>n,n)
	{
		int depth = 1;
		while (!dfs(1,depth))
		{
			depth++;
		}
		for (int i = 0; i < depth; i++)
			cout << path[i] << " ";
		cout << endl;
	}
	return 0;
}

7.2.5双向DFS

和双向BFS的原理差不多

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例:送礼物:

思路:

​ 1.将所有物品按重量从大到小排序

​ 2.先将前K件物品能凑出的所有重量打表,然后排序并判重

​ 3.搜索剩下的N-K件物品的选择方式,然后在表中二分出不超过W的最大值

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

typedef long long LL;

const int N = 46;

int n, m, k;
int w[N];
int weights[1 << 25], cnt = 1;
int ans;		//全局最大值
//第一段的dfs
void dfs1(int u,int s) {
	if (u == k) {
		weights[cnt++] = s;
		return;
	}

	dfs1(u + 1, s);
	if ((LL)s + w[u] <= m)dfs1(u + 1, s + w[u]);
}

void dfs2(int u, int s) {
	if (u == n) {
		int l = 0, r = cnt - 1;
		//二分
		while (l < r) {
			int mid = l + r + 1 >> 1;
			if (weights[mid] <= m - s) l = mid;
			else r = mid - 1;
		}
		ans = max(ans, weights[l] + s);
		return;
	}

	dfs2(u + 1, s);		//不选这个物品
	//选择这个物品
	if ((LL)s + w[u] <= m)
		dfs2(u + 1, s + w[u]);

}
int main() {
	cin >> m >> n;
	for (int i = 0; i < n; i++)
		cin >> w[i];

	sort(w, w + n);
	reverse(w, w + n);

	k = n / 2;
	dfs1(0, 0);

	sort(weights, weights + cnt);
	cnt = unique(weights, weights + cnt) - weights;
	dfs2(k, 0);

	cout << ans << endl;
}

该题的附加问题:二分搜索最靠近一个数的解时的边界问题:

解决方法:mid指针取(l+r+1)/2,小于等于这个数的解时左指针取mid,不再取mid+1,右指针取mid-1,循环条件为l<r,这样当跳出循环时l指向的数就是最小值

	int l = 0, r = cnt - 1;
		//二分
		while (l < r) {
			int mid = l + r + 1 >> 1;
			if (weights[mid] <= m - s) l = mid;
			else r = mid - 1;
		}
		ans = max(ans, weights[l] + s);

7.2.6IDA*

配合迭代加深使用:若当前深度加上估价函数大于最大深度则直接跳出

相当于配合迭代加深加上一个剪枝

保证估价函数小于等于真实值

例:排书

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <cstring>

using namespace std;

const int N = 15;

int n;
int q[N];  // 书的编号
int w[5][N];  // 恢复现场使用

// 估价函数
int f() {
    int cnt = 0;
    for (int i = 0; i + 1 < n; i++)
        if (q[i + 1] != q[i] + 1)
            cnt++;
    return (cnt + 2) / 3;
}

// 检查序列是否已经有序
bool check() {
    for (int i = 0; i + 1 < n; i++)
        if (q[i + 1] != q[i] + 1)
            return false;
    return true;
}

// k: 当前迭代深度; depth: 迭代加深最大深度
bool dfs(int depth, int max_depth) {

    if (depth + f() > max_depth) return false;
    if (check()) return true;

    for (int len = 1; len <= n; len++)  // 先遍历长度
        for (int l = 0; l + len - 1 < n; l++) {  // 再遍历左端点
            int r = l + len - 1;
            for (int k = r + 1; k < n; k++) {
                memcpy(w[depth], q, sizeof q);
                int x, y;
                // 将上图中绿色部分移动到红色部分
                for (x = r + 1, y = l; x <= k; x++, y++) q[y] = w[depth][x];
                // 将上图中红色部分移动到绿色部分
                for (x = l; x <= r; x++, y++) q[y] = w[depth][x];
                if (dfs(depth + 1, max_depth)) return true;
                memcpy(q, w[depth], sizeof q);
            }
        }
    return false;
}

int main() {

    int T;
    cin >> T;

    while (T--) {
        cin >> n;
        for (int i = 0; i < n; i++) cin >> q[i];

        int depth = 0;
        while (depth < 5 && !dfs(0, depth)) depth++;
        if (depth >= 5) puts("5 or more");
        else cout << depth << endl;
    }

    return 0;
}



八.常用stl

注: size()、empty()是所有容器都有的,时间复杂度为 O(1),并不是结果并非遍历得到,而是原本就有个变 量来存size,直接访问该变量即可

注:系统为某一程序分配空间时,所需时间与空间大小无关,而是与申请次数有关—倍增思想的原理

vector 变长数组,倍增的思想

size() 返回元素个数
empty() 返回是否为空
clear() 清空
front()/back() 
push_back()/pop_back()
begin()/end()
[] 即和数组一样,支持随机寻址
支持比较运算,按字典排序:
vector <int> a(3, 5), b(5,3);
if(a > b) cout << " a > b ";
遍历方式:

//遍历方法一
for(auto x:a)   cout << x << ' ';
//遍历方法二 迭代器可以看成是指针
for(int i = 0; i < a.size(); i ++)  cout << a[i] << ' ';
//遍历方法三 迭代器可以看成是指针
for(vector <int> :: iterator i = a.begin(); i != a.end(); i ++) cout << *i << ' ';

pair

pair <int, int>
first 第一个元素
second 第二个元素
支持比较运算, 以第一个为第一关键字, 第二个为第二关键字(字典序)---可用于按某一属性排序,将待排属性放
在第一个元素位置

pair 初始化方式:
    pair <string , int> p;
    p = {"hello",  20}
    p = make_pair("hello", 20);
    cout << p.first << ' ' << p.second ;

也可以用pair存储两个以上的属性,如:pair(int ,pair<int, int>);

string 字符串

substr(), c_str()  //c_str()  返回 const 类型的指针
size()/length() 返回字符串长度
empty()
clear()//清空整个字符串
erase() //erase(1,2) 删除以1为索引,长度为2的字符串
[]
支持比较运算,按字典序进行比较  a < "hello" 或 a.compare("hello");//a.compare() 返回具体的比较值

字符串变量和字符数组之间的转化:char ch[] = "hello"; string str = "world";
    ch[] -> str :   str = ch;
    str -> ch[] :   strcpy(ch,str.c_str());

string 初始化:
    string a("hello");
    string a = "hello";

取子串://很常用
    a.substr(1,3);//返回下标从1开始且长度为3的子串,包括左端点  

拼接字符串:
    a += "world";//新增字符串
    a.append(" world");//新增字符串
    a.push_back('.');//在字符串末新增单个字符

在字符串指定位置添加字符串
    a.insert(3,"world");

访问字符串:string str;
    cout << str[2];//以下标方式访问
    cout << str.at(2);//通过at()方法访问
    getline(cin,str );;//读取一行字符赋值给str
    getline(cin, str,'!');//读取一行字符赋值给str,以!结束

字符串排序:
    sort(str.begin(),str.end());//需要包含头文件algorithm

可以使用STL接口,可以理解为一个特殊的容器,容器里装的是的字符
    a.push_back('.');//在字符串末新增单个字符
    a.pop_back();

字符串变量的交换和取代:
    a.swap(str);//str 为字符串变量
    a.replace(1,2,str2) //用字符串str2取代字符串a下标为1长度为2的子串

queue 队列

size()
empty() 
push()  //向队尾插入一个元素
front() //返回对头元素
pop()   //弹出对头元素
back()  //返回队尾元素

priority_queue 优先队列
其实就是堆,默认是大根堆

push() //插入一个元素
top()  // 返回堆顶元素
pop()  //弹出堆顶元素

将小根堆转化为大根堆:
方法1: priority_queue<int,vector,greater> heap; //定义一个小根堆heap;
方法2: 以负数来存

stack

size()
empty()
push()  //向栈顶插入一个元素
top()   //返回栈顶元素
pop()   //弹出栈顶元素

deque 双端队列
缺点:慢,但用的不是很多,因为它要比一般的数组慢好几倍

size()
empty()
clear()
front() / back()
push_back() / pop_back()
push_front() / pop_front()
begin() / end()
[]

set, map, multiset, multimap
基于平衡二叉树(红黑树), 动态维护有序序列

set 与 multiset 的区别:set 里面不可以有重复元素,而multiset 可以有
size()
empty()
clear()
begin() / end() ++, -- 返回前驱和后继, 时间复杂度: O(logn)

set/multiset

insert() 插入一个数
find()   查找一个数
count()  返回某个数的个数
erase()
    注意:(1)(2)在set中无区别,但在multiset里有区别
    (1) 输入是一个整数x, 删除所有x          时间复发度: O(k  + logn)  //k是所有元素的个数
    (2) 输入一个迭代器, 删除这个迭代器

注意: lower_bound()/upper_bound() ----- 核心操作
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器

map/multimap

insert()  插入的一个数是一个pair         用的不多
erase()   输入的参数是pair 或 是迭代器   用的较多
find()
[]        时间复杂度: O(logn)           最主要的操作
lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap 哈希表

和上面类似,增、删、改、查的时间复杂度是 O(1) — 优势
和上面的区别:凡是和排序有关的操作都是不支持的,如:
不支持 lower_bound()/upper_bound() 迭代器的++,-- 等

bitset
压位, 存储相同的数据量,存储空间仅占bool变量的 1/8

定义变量: bitset<10000> s   //注意<>中存的不是类型,而是个数
~,&, |, ^
>> , <<
== , != 
[]
count()     返回有多少个1
any()       判断是否至少有一个1
none()      判断是否全为0
set()       把所有位置为1
set(k, v)   把第k位置为1
reset()     把所有位置为0
flip()      等价于~
flip(k)     把第k位取反

:: iterator i = a.begin(); i != a.end(); i ++) cout << *i << ’ ';

**pair**

```c++
pair <int, int>
first 第一个元素
second 第二个元素
支持比较运算, 以第一个为第一关键字, 第二个为第二关键字(字典序)---可用于按某一属性排序,将待排属性放
在第一个元素位置

pair 初始化方式:
    pair <string , int> p;
    p = {"hello",  20}
    p = make_pair("hello", 20);
    cout << p.first << ' ' << p.second ;

也可以用pair存储两个以上的属性,如:pair(int ,pair<int, int>);

string 字符串

substr(), c_str()  //c_str()  返回 const 类型的指针
size()/length() 返回字符串长度
empty()
clear()//清空整个字符串
erase() //erase(1,2) 删除以1为索引,长度为2的字符串
[]
支持比较运算,按字典序进行比较  a < "hello" 或 a.compare("hello");//a.compare() 返回具体的比较值

字符串变量和字符数组之间的转化:char ch[] = "hello"; string str = "world";
    ch[] -> str :   str = ch;
    str -> ch[] :   strcpy(ch,str.c_str());

string 初始化:
    string a("hello");
    string a = "hello";

取子串://很常用
    a.substr(1,3);//返回下标从1开始且长度为3的子串,包括左端点  

拼接字符串:
    a += "world";//新增字符串
    a.append(" world");//新增字符串
    a.push_back('.');//在字符串末新增单个字符

在字符串指定位置添加字符串
    a.insert(3,"world");

访问字符串:string str;
    cout << str[2];//以下标方式访问
    cout << str.at(2);//通过at()方法访问
    getline(cin,str );;//读取一行字符赋值给str
    getline(cin, str,'!');//读取一行字符赋值给str,以!结束

字符串排序:
    sort(str.begin(),str.end());//需要包含头文件algorithm

可以使用STL接口,可以理解为一个特殊的容器,容器里装的是的字符
    a.push_back('.');//在字符串末新增单个字符
    a.pop_back();

字符串变量的交换和取代:
    a.swap(str);//str 为字符串变量
    a.replace(1,2,str2) //用字符串str2取代字符串a下标为1长度为2的子串

queue 队列

size()
empty() 
push()  //向队尾插入一个元素
front() //返回对头元素
pop()   //弹出对头元素
back()  //返回队尾元素

priority_queue 优先队列
其实就是堆,默认是大根堆

push() //插入一个元素
top()  // 返回堆顶元素
pop()  //弹出堆顶元素

将小根堆转化为大根堆:
方法1: priority_queue<int,vector,greater> heap; //定义一个小根堆heap;
方法2: 以负数来存

stack

size()
empty()
push()  //向栈顶插入一个元素
top()   //返回栈顶元素
pop()   //弹出栈顶元素

deque 双端队列
缺点:慢,但用的不是很多,因为它要比一般的数组慢好几倍

size()
empty()
clear()
front() / back()
push_back() / pop_back()
push_front() / pop_front()
begin() / end()
[]

set, map, multiset, multimap
基于平衡二叉树(红黑树), 动态维护有序序列

set 与 multiset 的区别:set 里面不可以有重复元素,而multiset 可以有
size()
empty()
clear()
begin() / end() ++, -- 返回前驱和后继, 时间复杂度: O(logn)

set/multiset

insert() 插入一个数
find()   查找一个数
count()  返回某个数的个数
erase()
    注意:(1)(2)在set中无区别,但在multiset里有区别
    (1) 输入是一个整数x, 删除所有x          时间复发度: O(k  + logn)  //k是所有元素的个数
    (2) 输入一个迭代器, 删除这个迭代器

注意: lower_bound()/upper_bound() ----- 核心操作
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器

map/multimap

insert()  插入的一个数是一个pair         用的不多
erase()   输入的参数是pair 或 是迭代器   用的较多
find()
[]        时间复杂度: O(logn)           最主要的操作
lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap 哈希表

和上面类似,增、删、改、查的时间复杂度是 O(1) — 优势
和上面的区别:凡是和排序有关的操作都是不支持的,如:
不支持 lower_bound()/upper_bound() 迭代器的++,-- 等

bitset
压位, 存储相同的数据量,存储空间仅占bool变量的 1/8

定义变量: bitset<10000> s   //注意<>中存的不是类型,而是个数
~,&, |, ^
>> , <<
== , != 
[]
count()     返回有多少个1
any()       判断是否至少有一个1
none()      判断是否全为0
set()       把所有位置为1
set(k, v)   把第k位置为1
reset()     把所有位置为0
flip()      等价于~
flip(k)     把第k位取反
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值