Algorithm

Algorithm

代码随想录:

代码随想录 (programmercarl.com)

一、怎么解决问题

img

二、思想技巧

1.矩阵转换为一维

表示方法.JPG

矩阵转换.JPG

int x = k / 3, y = k % 3;//一维->二维
int t= x*3+y;//二维->一维

2.滚动数组

滚动数组是一种能够在动态规划中降低空间复杂度的方法,有时某些二维dp方程可以直接降阶到一维,在某些题目中甚至可以降低时间复杂度,是一种极为巧妙的思想。

简要来说就是通过观察dp方程来判断需要使用哪些数据,可以抛弃哪些数据,一旦找到关系,就可以用新的数据不断覆盖旧的数据量来减少空间的使用。

以0-1背包举例:

dp方程递推过程是不断地由上一行的数据传递到下一行,即一层状态向另一层状态转移

比如 dp[5][10]的状态是由 dp[5][10]=max(dp[4][10] , dp[4][9] + 5 ) = 14 递推得到的,当我们递推到dp[i][j]时,对于那些只要求最终最佳答案的情况来说,只需要 i−1 这行的数据即可,至于上面的 i − 1 , i − 2 ,...,1 都是不需要的数据。
所以我们可以只用一维数组dp[j] 来记录数据dp[i][j] 的状态,在更新的过程中不断用新的数据dp[j](dp[i][j])覆盖掉旧的数据dp[j](dp[i−1][j])

3.Lmada表达式

C ++ Lambda表达式详解_c++ lambda-CSDN博客

4.类型大小

类型大约范围
int − 2 ∗ 1 0 9 → 2 ∗ 1 0 9 − 2 31 → 2 31 -2*10^{9} \to 2*10^{9} \\-2^{31} \to 2^{31} 21092109231231
long long 9 ∗ 1 0 18 → 9 ∗ 1 0 18 − 2 63 → 2 63 9*10^{18} \to 9*10^{18}\\-2^{63} \to 2^{63} 9101891018263263

5.输入输出

当数据规模<= 1 0 5 10^5 105时,cin和scanf一样,当超过 1 0 5 10^5 105时用scanf更快

5.有数学公式的

可以先对数学公式进行变形(尤其是带除法的)

可以移项、化简等等

6.异或取反操作

用异或1来实现,对任何的数都可以使用,一般是先转换为二进制再进行异或操作,得到结果

对5进行异或1,就是(101)异或(001)得到4(100)

对0和1进行异或1,就是取反了,0变1,1变0

a^=b;//将a和b进行异或操作,结果给a

7.位运算来限制枚举

如果n个空的取值是k种,那么所有可能的情况是 k n k^n kn,而此时枚举就可以从0到 k n − 1 k^n-1 kn1进行枚举

8.向上取整/向下取整

(1)floor()和ceil()

floor()函数用于向下取整,而ceil()函数用于向上取整。以下是关于这两个函数的详细解释:

  • floor()函数:此函数会将传入的小数值向下取整到最接近的整数。如果数值已经是整数或者小数部分小于0.5,则返回该数本身;如果小数部分大于或等于0.5,则返回该数减去1的结果。例如,floor(2.2)将返回2.0,而floor(-2.2)将返回-3.0。这是因为-2.2向下取整会趋向于更小的整数值。
  • ceil()函数:与floor()相反,ceil()函数会将小数值向上取整到最接近的整数。若小数部分大于0,即使只有0.1,也会返回整数部分加1的结果;如果小数部分小于0.5,则直接返回该数的整数部分。例如,ceil(2.2)将返回3.0,而ceil(-2.2)将返回-2.0。对于正数来说,ceil()总是返回不小于原数的最小整数。

(2)强制类型转换

向上取整:

if(res>(int)res)
{
    cout << (int)res + 1;
}
else
{
    cout << (int)res;
}

向下取整:

if(res>(int)res)
{
    cout << (int)res - 1;
}
else
{
    cout << (int)res;
}

(3)判断是不是小数

看res与(int)res的大小,如果不相等则是有小数部分

(4)除法向上/向下取整

C++自动向下取整

向上取整:

a/b的向上取整 == (a+b-1)/b

9.重载运算符

一般来讲会加上第二个const,保证编译时不会出错。

类型名+operator+要重载的运算符+(const 类型名& 对象名)const{ }。

struct Point {
    int x;
    int y;

    Point operator+(const Point& other) const {
        Point result;
        result.x = x + other.x;
        result.y = y + other.y;
        return result;
    }
};

10.调试技巧

(1)Swgmentation Fault

exit(0):直接正常退出

可以把这个函数放在最后面,然后放在中间,不断二分,直到不报错,找到问题

11.矩阵变换

矩阵的横和列不一定是垂直的,还可能是斜着的。

image-20240403203724940

此时可以对矩阵的行和列进行抽象的变换一下。

12.取模运算

如果MOD== 1e+9,那么在进行取模运算的时候,只能有两个数进行取模运算,不能有3个

因为int 类型大小是 2 ∗ 1 0 9 2*10^9 2109,所以只能有两个 1 0 9 10^9 109的数进行运算,再多就会爆int

13.排列

如果题目中出现了某个区间是排列,那么这个区间中没有重复的值。

14.连号

如果一个区间是连号的,那么就会满足性质:max-min== R-L[[连号区间]]

15.求数组中比i大的有几个,比i小的有几个

没有等于,只有大于和小于

int a[N], b[N], c[N];
sort(a, a + n);
sort(b, b + n);
sort(c, c + n);
for (int i = 0; i < n; i++)//对b数组进行遍历
    {
        if(b[i]==0)//如果都是正的,b[i]是0,那么没有意义
            continue;//如果存在负数,是有意义的
        int an = lower_bound(a, a + n, b[i]) - a;
        //求的是比b[i]小的数有几个
        int cn = upper_bound(c, c + n, b[i]) - c;
        //n-cn求的是比b[i]大的数有几个
        cnt += an * (n - cn);
    }

16.读入技巧

(1)当没有指定列数的时候,可以用[[错误的票据]]

while(cin>>tmp)
{
	int n = tmp;
}

(2)当结束标志是以1个或者多个0结束时[[红与黑]]

while(cin>>n>>m n||m)
{

}

三、开启优化

(1)O 2优化

#pragma GCC optimize(2);

(2)O 3优化

#pragma GCC optimize(3,"Ofast","inline");

(3)关闭I/O同步

ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);

四、String

1.cin

最为常用的输入函数,接收一个string/char[ ],遇到空格/TAB/回车结束

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string a, b;
	cin >> a >> b;
	cout << a << endl;
	cout << b << endl;
	return 0;
}

2.getline()

接受一个string/char [ ],可以接收空格并输出,需包含 include<string>

#include<iostream>
#include<string>
using namespace std;
int main ()
{
    string str;
    getline(cin,str);
    cout<<str<<endl;
}

3.swap()

str1.swap(str2);

4.sort()

const int N=100;
string str[N];
while(m--)
{
    cin>>str[i];
}
sort(str,str+N);//会按照ASCII进行升序排列

bool cmp(string a,string b)
{
    return a.length()<b.length();
    //按长度升序排列
}
sort(str,str+N,cmp);

//对string内部进行排序
string str="45321";
sort(str.begin(),str.end());//结果为12345

4.scanf()

#include <stdio.h>
#include <string>
#include<iostream>
using namespace std;
int main()
{
	string a;
	a.resize(2); //需要预先分配空间
	scanf("%s", &a[0]);
	cout << a;
	return 0;
} 
//输入:hello
//输出:he
char op[2];
//用scanf读入一个字母时,用字符串数组的话,scanf()会自动忽略掉一些额外的空格和回车
sacnf("%s",op);

5.vector+string

vector+string: 创造出来的string类型的数组会拥有两者的特点

vector<string> str={"333","222","111","555","444"};
sort(str.begin(),str.end());
for(int i=0;i<str.size();i++)
{
    cout<<str[i]<<" ";
}
//结果为:111 222 333 444 555

vector<string> str={"333","222","111","555","444"};
sort(str.begin(),str.end());
for(int i=0;i<str.size();i++)
{
    cout<<str[i]<<" ";
}
cout<<endl;
str.push_back("1010");
cout<<str[5]<<endl;
sort(str.begin(),str.end());
for(int i=0;i<str.size();i++)
{
    cout<<str[i]<<" ";
}
//结果为:
//111 222 333 444 555
//1010
//1010 111 222 333 444 555

6.string字符串的比较

C ++字符串支持常见的比较操作符(>,>=,<,<=,==,!=),甚至支持string与C-string的比较(如 str<”hello”)。
在使用>,>=,<,<=这些操作符的时候是根据“当前字符特性”将字符按字典顺序进行逐一得 比较。字典排序靠前的字符小,
比较的顺序是从前向后比较,遇到不相等的字符就按这个位置上的两个字符的比较结果确定两个字符串的大小(前面减后面)
同时,string (“aaaa”) <string(aaaaa)。

7.push_back()和insert()

void  test4()
{
    string s1;

    // 尾插一个字符
    s1.push_back('a');
    s1.push_back('b');
    s1.push_back('c');
    cout<<"s1:"<<s1<<endl; // s1:abc

    // insert(pos,char):在制定的位置pos前插入字符char
    s1.insert(s1.begin(),'1');
    cout<<"s1:"<<s1<<endl; // s1:1abc
}

8.拼接

append 或者是 +

// 方法一:append()
string s1("abc");
s1.append("def");
cout<<"s1:"<<s1<<endl; // s1:abcdef

// 方法二:+ 操作符
string s2 = "abc";
/*s2 += "def";*/
string s3 = "def";
s2 += s3;
//或者是s2 += s3.c_str();
//s3转换为c字符串
cout<<"s2:"<<s2<<endl; // s2:abcdef

9.erase()

1. iterator erase(iterator p);//删除字符串中p所指的字符

2. iterator erase(iterator first, iterator last);//删除字符串中迭代器

区间[first,last)上所有字符

3. string& erase(size_t pos = 0, size_t len = npos);//删除字符串中从索引

位置pos开始的len个字符

4. void clear();//删除字符串中所有字符
void test6()
{
    string s1 = "123456789";


    s1.erase(s1.begin()+1);              // 结果:13456789
    s1.erase(s1.begin()+1,s1.end()-2);   // 结果:189
    s1.erase(1,6);                       // 结果:189
    string::iterator iter = s1.begin();
    while( iter != s1.end() )
    {
        cout<<*iter;
        *iter++;
    }
    cout<<endl;
}

10.replace()

1. string& replace(size_t pos, size_t n, const char *s);//将当前字符串从pos索引开始的n个字符,替换成字符串s

2. string& replace(size_t pos, size_t n, size_t n1, char c); //将当前字符串从pos索引开始的n个字符,替换成n1个字符c

3. string& replace(iterator i1, iterator i2, const char* s);//将当前字符串[i1,i2)区间中的字符串替换为字符串s

11.find()

1. size_t find (constchar* s, size_t pos = 0) const;

  //在当前字符串的pos索引位置开始,查找子串s,返回找到的位置索引,

    -1表示查找不到子串

2. size_t find (charc, size_t pos = 0) const;

  //在当前字符串的pos索引位置开始,查找字符c,返回找到的位置索引,

    -1表示查找不到字符

3. size_t rfind (constchar* s, size_t pos = npos) const;

  //在当前字符串的pos索引位置开始,反向查找子串s,返回找到的位置索引,

    -1表示查找不到子串

4. size_t rfind (charc, size_t pos = npos) const;

  //在当前字符串的pos索引位置开始,反向查找字符c,返回找到的位置索引,-1表示查找不到字符

5. size_tfind_first_of (const char* s, size_t pos = 0) const;

  //在当前字符串的pos索引位置开始,查找子串s的字符,返回找到的位置索引,-1表示查找不到字符

6. size_tfind_first_not_of (const char* s, size_t pos = 0) const;

  //在当前字符串的pos索引位置开始,查找第一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到字符

7. size_t find_last_of(const char* s, size_t pos = npos) const;

  //在当前字符串的pos索引位置开始,查找最后一个位于子串s的字符,返回找到的位置索引,-1表示查找不到字符

8. size_tfind_last_not_of (const char* s, size_t pos = npos) const;

 //在当前字符串的pos索引位置开始,查找最后一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到子串
void test8()
{
    string s("dog bird chicken bird cat");

    //字符串查找-----找到后返回首字母在字符串中的下标

    // 1. 查找一个字符串
    cout << s.find("chicken") << endl;        // 结果是:9

    // 2. 从下标为6开始找字符'i',返回找到的第一个i的下标
    cout << s.find('i',6) << endl;            // 结果是:11

    // 3. 从字符串的末尾开始查找字符串,返回的还是首字母在字符串中的下标
    cout << s.rfind("chicken") << endl;       // 结果是:9

    // 4. 从字符串的末尾开始查找字符
    cout << s.rfind('i') << endl;             // 结果是:18-------因为是从末尾开始查找,所以返回第一次找到的字符

    // 5. 在该字符串中查找第一个属于字符串s的字符
    cout << s.find_first_of("13br98") << endl;  // 结果是:4---b

    // 6. 在该字符串中查找第一个不属于字符串s的字符------先匹配dog,然后bird匹配不到,所以打印4
    cout << s.find_first_not_of("hello dog 2006") << endl; // 结果是:4
    cout << s.find_first_not_of("dog bird 2006") << endl;  // 结果是:9

    // 7. 在该字符串最后中查找第一个属于字符串s的字符
    cout << s.find_last_of("13r98") << endl;               // 结果是:19

    // 8. 在该字符串最后中查找第一个不属于字符串s的字符------先匹配t--a---c,然后空格匹配不到,所以打印21
    cout << s.find_last_not_of("teac") << endl;            // 结果是:21

}

12.分割strtok()

void test10()
{
    char str[] = "I,am,a,student; hello world!";

    const char *split = ",; !";
    char *p2 = strtok(str,split);
    while( p2 != NULL )
    {
        cout<<p2<<endl;
        p2 = strtok(NULL,split);
    }
}
/*
I
am
a
student
hello
world
*/

13.截取substr()

返回一个子串

substr(int begin , int length);//begin为起始位置,length为字串的长度,当长度超过最后一个字符时,就返回begin->母串的end

string s1("0123456789");
string s2 = s1.substr(2,5); // 结果:23456-----参数5表示:截取的字符串的长度
cout<<s2<<endl;

substr(int a);//如果只有一个参数的话,会返回 a->母串的end

14.empty()

判断是否为空

15.clear()

16.运算

string a = "abcd";
a+="efg";
//a="abcdefg";

17.c_str()

a.c_str();
//返回的时a字符数组的起始地址
cout<<a.c_str();
//a="abcdefg";

五、ST L模板库

时间复杂度为O(logn)

1. sort函数

//函数原型:
default (1):
    template <class RandomAccessIterator>
    void sort (RandomAccessIterator first, RandomAccessIterator last);
custom (2):
    template <class RandomAccessIterator, class Compare>
    void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);



sort(a,a+n);//对a从小到大排序


bool cmp(int a1,int a2)
{
    return a1>a2;//大于号>,为降序排列,小于号<,为升序排列
}
int a[];
sort(a,a+n,cmp);//对a从大到小排序

2.min函数

//函数原型:
template <class T, class Compare>
bool comp(const T& a, const T& b);// 按定义的比较规则返回a<b的真值
const T& min (const T& a, const T& b, Compare comp);

#include<algorithm>
using namespace std;
int c=min(a,b);//C为a,b中较小的一个

min({a, b, c, d, e}); //取a,b,c,d,e五个变量中的最小值

int a[5] = { 1, 3, 2, 0 };
cout << *min_element(a,a+4) << endl; //取数组下标从 0~3 中的最小值,min返回的是地址,所以要加*
//左闭右开

3.max函数

//函数原型:
default (1)template <class T> const T& max (const T& a, const T& b);
custom (2)template <class T, class Compare>
    const T& max (const T& a, const T& b, Compare comp);




#include<algorithm>
using namespace std;
int c=max(a,b);//C为a,b中较大的一个
max({a, b, c}); //取a,b,c三个变量中的最大值

int a[5] = { 1, 3, 2, 0 };
cout << *max_element(a,a+3) << endl; //取数组下标从 0~2 中的最大值
//左闭右开

4.minmax函数

//函数原型:
default (1)template <class T>
    pair <const T&,const T&> minmax (const T& a, const T& b);
custom (2)template <class T, class Compare>
    pair <const T&,const T&> minmax (const T& a, const T& b, Compare comp);



#inlcude<algorithm>
using namespace std;
int a[5] = { 1, 3, 2, 0 };
auto result =minmax(a,a+5);//返回first,second两个值
result.first;//为最小值
result.second;//为最大值

//minmax_element函数
int a[5] = {1, 2, 3, 4, 5};
auto result = minmax_element(a,a+5);
cout << *result.first << " " << *result.second << endl;
vector<int> number = {1, 2, 3, 4, 5};
auto r = minmax_element(number.begin(), number.end());
cout << *r.first << " " << *r.second<< endl;
    

5.二分查找函数:binary_search()

//函数原型:
default (1):
    template <class ForwardIterator, class T>
    bool binary_search (ForwardIterator first, ForwardIterator last, const T& val);

custom (2):
    template <class ForwardIterator, class T, class Compare>
    bool binary_search (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);

#include<algorithm>
using namespace std;

int a[];
sort(a,a+n);
bool T=binary_search(a,a+n,number);//number为要查找的值

  1. equal_range函数

//函数原型:
default (1):
    template <class ForwardIterator, class T>
    pair<ForwardIterator,ForwardIterator>
    equal_range (ForwardIterator first, ForwardIterator last, const T& val);
custom (2):
    template <class ForwardIterator, class T, class Compare>
    pair<ForwardIterator,ForwardIterator>
    equal_range (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);
//综合了lower_bound和upper_bound的能力



#include<algorithm>
using namespace std;

int arr[5] = {1,2,2,4,5};
auto bounds = equal_range(arr, arr+5, 2);
int a = bounds.first-arr; // a结果为1
//返回的是第一个大于等于number的值的地址

int b = bounds.second-arr; // b结果为3
//返回的是第一个大于number的值的地址,如果要求最后一个number的位置,那么要再减一

7.复制函数:copy()、copy_n()和memcpy

//copy的函数原型:
template <class InputIterator, class OutputIterator>
OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result);
//copy()会复制整个数组到新的数组中
int arr1[4] = {1,3,2,4};
int arr2[4];
copy(arr1, arr1+4, arr2);//arr1是被复制的数组,arr2是复制的数组
//自动修改数组arr2



//copy_n的函数原型:
template <class InputIterator, class Size, class OutputIterator>
OutputIterator copy_n (InputIterator first, Size n, OutputIterator result);
//参数first为数组首地址,参数n为要复制的数组元素个数,参数result为新数组首地址
//copy_n会复制size个
int arr1[4] = {1,3,2,4};
int arr2[4];
copy_n(arr1, 4, arr2);
//自动修改数组arr2


mmecpy是将源内存块复制到目标内存块。
memcpy(back,g,sizeof g);
//将g复制到back数组,可以复制二维数组

8.交换函数swap()

//函数原型
template <class T> void swap (T& a, T& b);
//传入两个参数地址引用,功能是交换这两个参数的数值
int a=1, b=2;
swap(a, b);
//a结果为2,b结果为1
//自动修改

9.取代函数replace()

//函数原型
template <class ForwardIterator, class T>
void replace (ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value);
//传入参数first为数组首地址,参数last为数组尾地址,要被替换的旧元素为参数old_value,替换的新的元素为参数new_value,函数功能是将数组中所有的old_value被替换为new_value

int arr[4] = {1,2,2,3};
replace(arr, arr+4, 2, 0);
//arr结果为{1,0,0,3}
//自动修改数组arr

10.填充函数fill()

//函数原型
template <class ForwardIterator, class T>
void fill (ForwardIterator first, ForwardIterator last, const T& val);
//传入参数first为数组首地址,参数last为数组尾地址,填充值为参数val,函数功能是将数组中的所有元素都重新赋值为val

int arr[4] = {1,2,2,3};
fill(arr, arr+4, 5);
//arr结果为{5,5,5,5}
//自动修改数组arr

11.倒置函数reverse()

//函数原型
template <class BidirectionalIterator>
void reverse (BidirectionalIterator first, BidirectionalIterator last);
//传入参数first为数组首地址,参数last为数组尾地址,函数功能是将数组中的所有元素对称交换

int arr[4] = {1,2,3,4};
reverse(arr, arr+4);
//arr结果为{4,3,2,1}
//自动修改数组arr

12.滚动函数rotate()

//函数原型
template <class ForwardIterator>
ForwardIterator rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last);
//传入参数first为数组首地址,参数last为数组尾地址,而参数middle则是数组中要滚动的最后一个元素的后一个地址,也就是地址意义上的第middle个数,滚动完成后该地址将成为首地址

int arr[5] = {0,1,2,3,4};
rotate(arr, arr+3, arr+5);
//arr结果为{3,4,0,1,2}
//自动修改数组arr

13.无序数组查找指定元素find()

//函数原型
template <class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val);
//在无序数组中的查找指定元素x,若存在则返回第一个x所在的地址,否则返回数组尾地址

int arr[4] = {1,3,2,3};
int *p = find(arr, arr+4, 3); // p结果为地址arr+1
int *q = find(arr, arr+4, 0); // q结果为地址arr+4

14.无序数组查找指定数组子序列find_end()

//函数原型:
template <class ForwardIterator1, class ForwardIterator2>
ForwardIterator1 find_end (ForwardIterator1 first1, ForwardIterator1 last1, ForwardIterator2 first2, ForwardIterator2 last2);
//在无序数组arr1中的查找指定子数组arr2是否存在,若存在则返回待查子数组arr2最后出现在原数组arr1的地址,也就是匹配到的arr2头指针所在的位置,否则返回原数组的尾地址

int arr[5] = {0,3,4,3,4};
int arr1[2] = {3,4};
int arr2[2] = {3,5};
int *p = find_end(arr, arr+5, arr1, arr1+2); // p结果为地址arr+3
int *q = find_end(arr, arr+5, arr2, arr2+2); // q结果为地址arr+5

15.指定元素个数统计count()

//函数原型:
template <class InputIterator, class T>
typename iterator_traits<InputIterator>::difference_type count (InputIterator first, InputIterator last, const T& val);
//在数组中统计指定元素x出现的次数,传入参数first为数组首地址,参数last为数组尾地址,参数x为待统计的指定元素

int arr[5] = {0,3,4,3,4};
int cnt = count(arr, arr+5,3); // cnt结果为2

16.两个数组相等比较equal()

//函数原型:
template <class InputIterator1, class InputIterator2>
bool equal (InputIterator1 first1, InputIterator1 last1, InputIterator2 first2);
//比较两个数组是否相等,返回比较真值,其函数原型及其应用实例如下,其中参数first1是第一个数组的首地址,参数last1是第一个数组的尾地址,参数first2是第二个参数的首地址,默认两个数组元素个数是相同的,否则没有比较意义

int arr1[2] = {3,4};
int arr2[2] = {3,4};
int arr3[2] = {3,5};
bool judge1 = equal(arr1, arr1+2, arr2); // judge1结果为地址true
bool judge2 = equal(arr1, arr1+2, arr3); // judge2结果为地址false

17.下一个排列模板函数 next_permutation()

//函数原型:
template <class BidirectionalIterator>
bool next_permutation (BidirectionalIterator first,   BidirectionalIterator last);
//产生该排列的下一个序列,输入参数为序列的首地址和尾地址
//也就是对数组中原有的元素进行全排列,但是会排除数组原有的排列
//下一个排列就是,对于一个排列从小到大,根据数组原来的排列方式,选取下一个排列方式,然后循环

int myints[] = {1,2,3};
do {
    std::cout << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';
} while ( std::next_permutation(myints,myints+3) );
//想要输出所有的排列方式,用do while()循环

 do
   {
       for(int j=0;j<n-1;j++)
       {
           cout<<arr[j]<<" ";
       }
       cout<<arr[n-1];
       cout<<endl;
       m--;
   }while(next_permutation(arr,arr+n) && m >0);
//m可以控制输出的排列的个数,一共输出m个排列

18.上一个排列模板函数 prev_permutation()

//函数原型:
template <class BidirectionalIterator>
bool prev_permutation (BidirectionalIterator first,   BidirectionalIterator last);
//产生该排列的上一个序列,输入参数为序列的首地址和尾地址
//也就是对数组中原有的元素进行全排列,但是会排除数组原有的排列
//下一个排列就是,对于一个排列从大到小,根据数组原来的排列方式,选取下一个排列方式,然后循环

int myints[] = {3,2,1};
do {
    std::cout << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';
} while ( std::prev_permutation(myints,myints+3) );
//想要输出所有的排列方式,用do while()循环

    do
   {
       for(int j=0;j<n-1;j++)
       {
           cout<<arr[j]<<" ";
       }
       cout<<arr[n-1];
       cout<<endl;
       m--;
   }while(prev_permutation(arr,arr+n) && m >0);
//m可以控制输出的排列的个数,一共输出m个排列
      

19.判断是否为字母isalpha()

不区分大小写,包含于ctype.h头文件,也包含在iostream中

#include<cstdio> 
#include<cstring>
#include<cctype>
const int maxn = 210;
int main(){
	char str[maxn];
	while(gets(str)){
		int len = strlen(str);
		for(int i=0;i<len;i++){
			if(isalpha(str[i])) 
				printf("%c",str[i]);
		}
		printf("\n");
	}
	return 0;
}

20.判断是否为数字isdigit()

判断字符是否为数字,属于ctype.h头文件;但也包含在iostream头文件下

#include<cstdio> 
#include<ctype.h>
int main(){
	char a[] = "wo1A2i3X4";
	int i=0;
	while(a[i]){
		if(isdigit(a[i])) 
			printf("%c",a[i]);
		i++; 
	}
	return 0;
}

21.nth_element

时间复杂度为O(N);

nth_element()方法,默认是求区间第k小的

nth_element(a,a+k-1,a+n),函数只是把下标为k-1的元素放在了正确位置,对其它元素并没有排序,当然k左边元素都小于等于它,右边元素都大于等于它,所以可以利用这个函数快速定位某个元素。

求第k大时,可以转化成求第n-k+1小,此时下标应该是n - k。

nth_element(a,a+n-k,a+n),将下标为n-k,也就是第n-k+1个数放在正确的位置,求的是第k大的数a[n-k]。

#include<bits/stdc++.h>
using namespace std;
bool cmp(int a, int b){
    return a > b;
}
int main()
{
	int a[9] = {4,7,6,9,1,8,2,3,5};
	int b[9] = {4,7,6,9,1,8,2,3,5};
	int c[9] = {4,7,6,9,1,8,2,3,5};
	nth_element(a,a+2,a+9);
	//将下标为2,也就是第3个数放在正确的位置
	//也就是求的是第3小
	cout <<"第3小是:"<< a[2] << endl;
	for(int i = 0; i < 9; i++)
    cout << a[i] << " "; puts("");//注意下标是从0開始计数的
	//那么求第3大,就是求第9-3+1小,即第7小
	//也就是将下标为6的第7个数,放在正确的位置
	nth_element(b,b+6,b+9);
	cout <<"第3大是:"<< b[6] << endl;
	for(int i = 0; i < 9; i++)
	cout << b[i] << " "; puts("");//注意下标是从0開始计数的
	nth_element(c,c+2,c+9,cmp);//第一种方法
	//nth_element(c,c+2,c+9,greater<int>()); //第二种方法
	cout <<"第3大是:"<< c[2] << endl;
	for(int i = 0; i < 9; i++)
	cout << c[i] << " "; //注意下标是从0開始计数的
}
/*总结:
求第k小的数:nth_element(a,a+k-1,a+n);
求第k大的数:nth_element(a,a+n-k,a+n);
*/

求第k小的数:nth_element(a,a+k-1,a+n);
求第k大的数:nth_element(a,a+n-k,a+n);

22.初始化memset()

void *memset(void *str, int c, size_t n) 复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符

时间复杂度为O(N);

常见错误:
第一:memset函数按字节对内存块进行初始化,所以不能用它将int数组初始化为0和-1之外的其他值(除非该值高字节和低字节相同)。

第二:memset(void *s, int ch,size_t n);中ch实际范围应该在0~~255,因为该函数只能取ch的后八位赋值给你所输入的范围的每个字节比如int a[5]赋值memset(a,-1,sizeof(int )*5)与memset(a,511,sizeof(int )*5) 所赋值的结果是一样的都为-1;因为-1的二进制码为(11111111 11111111 11111111 11111111)而511的二进制码为(00000000 00000000 00000001 11111111)后八位都为(11111111),所以数组中每个字节,如a[0]含四个字节都被赋值为(11111111),其结果为a[0](11111111 11111111 11111111 11111111),即a[0]=-1,因此无论ch多大只有后八位二进制有效,而后八位二进制的范围在(0~255)中改。而对字符数组操作时则取后八位赋值给字符数组,其八位值作为ASCII码。

对于数组初始化为无穷大

memset(a,0x3f,sizeof a);
int a[N][N];
memset(a,0,sizeof(a));
//对于int型数组,只能初始化为0/-1

char buffer[4];
memset(buffer,0,sizeof(char)*4);

23.lower_bound和upper_bound

在从小到大的排序数组中,

lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

在从大到小的排序数组中,重载lower_bound()和upper_bound()

lower_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

int pos1=lower_bound(num,num+6,7)-num;    //返回数组中第一个大于或等于被查数的值的下标
int pos2=upper_bound(num,num+6,7)-num;    //返回数组中第一个大于被查数的值的下标

sort(num,num+6,cmd);                      //按从大到小排序
int pos3=lower_bound(num,num+6,7,greater<int>())-num;  //返回数组中第一个小于或等于被查数的值的下标
int pos4=upper_bound(num,num+6,7,greater<int>())-num;  //返回数组中第一个小于被查数的值的下标

24.binary_search

//查找 [first, last) 区域内是否包含 val
bool binary_search (ForwardIterator first, ForwardIterator last,const T& val);
//根据 comp 指定的规则,查找 [first, last) 区域内是否包含 val
bool binary_search (ForwardIterator first, ForwardIterator last,const T& val, Compare comp);

25.gcd

求两个整数最大公约数的函数,即std::gcd。这个函数定义在<algorithm>头文件中

需要注意的是,std::gcd函数仅适用于整数类型,不支持浮点数类型。此外,std::gcd函数是C++17标准引入的

int a = 12;
int b = 18;
int result = std::__gcd(a, b);

六、STL容器

1.vector容器:

(1)定义

一种顺序容器,与数组类似,可动态分配和拓展内存,它的随机访问快,在中间插入和删除慢,但在末端插入和删除快。

#include<vector>
//定义:
vector<int> v1,定义一个元素为类型为int 整型的向量v1
    
vector<string> v2(10),定义一个元素为类型为string字符串类型的向量v2,初始存储空间大小为10,每个元素初始为空串
    
vector<node> v3,定义一个元素为类型为node类型的向量v3,其中node一般是结构体等自定义数据类型;    
    
(2)插入元素
往向量插入一个元素通过调用push_back()方法实现(在向量末尾插入),也可以通过下标访问的方式直接在指定位置插入元素(前提是该位置已经被分配内存空间);
vector <int> vec; // 创建一个整型向量vec
vec.push_back(1); // 向vec插入一个元素1
vec.push_back(2); // 向vec插入一个元素2
vec[1] = 3; // 直接在位置1插入元素3,原来的元素2被元素3覆盖了
// 目前vec包含 1, 3两个元素
(3)删除元素
队尾删除通过调用pop_back()方法,注意,它并不会返回被删除的元素;
vec.pop_back(); // 删除了元素3



指定位置的删除是基于迭代器iterator实现的,迭代器相对应数组的指针,指向向量的存储地址,通过调用erase(iterator pos)方法删除迭代器位置pos所在的元素
vector<int>::iterator pos = vec.begin(); //定义一个vector<int>的迭代器pos,并指向vec的首地址
cout<<*pos; // 与指针一样,通过*访问地址上的值,输出为1
vec.erase(pos); // 删除迭代器地址pos及其元素,目前pos为vec首地址,元素值为1,删除之后元素为空,不包含任何元素了
(4)sort排序
sort(vec.begin(), vec.end()); // 默认从小到大排序
(5)遍历向量
//下标访问
for(int i=0;i<vec.size();i++) // size()返回当前向量vec的大小
    cout<<vec[i];

//迭代器访问
for(vector<int>::iterator it=vec.begin();it!=vec.end();it++)
    cout<<*it;
(6)清空向量
vec.clear()//清空后向量大小变为0
(7)初始化
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

int main(int argc, const char * argv[])
{
    vector<int> vec;
    int n,a;
    cin>>n;
    for(int i=0;i<n;i++)
    {
    	cin>>a;
        vec.push_back(a);
    }
    sort(vec.begin(),vec.end());
    vec.erase(unique(vec.begin(),vec.end()),vec.end());
    for(int i=0;i<vec.size()-1;i++)
    {
        cout<<vec[i]<<" ";
    }
    cout<<vec[vec.size()-1]<<endl;
    vec.clear();
    printf("%d\n", int(vec.size()));
    return 0;
}

(8)二维vector
//定义:
vector<vector<int> >vec;//空格不要忘记

//初始化:
for(int i=0;i<n;i++)
    {
        cin>>num;//每一行的大小的num
        for(int j=0;i<num;j++)
        {
            vec[i].push_back();
        }

    }

(9)front / back
//front 返回第一个元素
//back  返回最后一个元素
(10)begin / end
//begin 是vector 的第一个迭代器
//end 是最后一个元素的下一个迭代器
(11)支持随机访问
vector<int>  a;
a[0];
a[1];
(12)支持比较运算

这个比较运算默认是按照,字典序进行排列(也就是ASCII)。

vector<int> a(3,4),b(4,3);
//a=444,b=3333
//此时 a 是 > b 的。
//因为在ASCII中4>3

2.set容器:

(1)定义

集合set就是数学上的集合,其中的每个元素没有重复的,但是set中的元素在数据结构中是有序存储的(默认升序)

其底层数据结构是基于平衡搜索树(红黑树)实现的,插入删除等操作都是通过迭代器指针实现的,不涉及内存操作,因此效率非常高。

//定义
set<int> st;
(2)插入元素insert()

通过insert()函数实现

set<int> st; // 创建一个整型集合st
st.insert(1); // 向st插入一个元素1
st.insert(2); // 向st插入一个元素2
(3)删除元素erase()

集合元素的删除通过调用erase()方法实现,传入的参数可以是待删除的元素,也可以是待删除元素的地址:

set<int>::iterator it = st.begin(); // 定义一个迭代器,初始为st的首地址
cout<<*it; // 输出为元素1
st.erase(it); // 删除it这个迭代器
st.erase(2); // 删除所有的元素2
//时间复杂度为O(k+ logn),k为删除元素的个数
(4)遍历集合iterator
集合的遍历通过迭代器的方式进行,首先让迭代器指针指向集合的首地址,然后逐步移动迭代器指针,直到集合的尾地址;
for(set<int>::iterator it = st.begin();it!=st.end();it++)
    cout<<*it;
//也可以写成auto it = st.begin()
(5)查找元素find

查找指定元素通过find()方法实现,若找到了则返回该元素在集合中的地址,否则返回集合的尾地址

it = find(2);//查找指定元素2,it结果为st.end(),因为2已经被删除了
(6)集合清空clear()

通过调用clear()方法实现,清空后集合的大小st.size()变为0

st.clear()
cout<<st.size(); // 结果为0
(7)multiset()

set中不允许有重复元素,multiset中允许有重复元素

(8)count

返回元素的个数,multiset会有几个就返回几个

set<int>a(0,0,1,1,2);
a.count(0);//返回1,set中没有重复元素,所以count只会返回0或者1
multiset<int>s(0,0,1,1,2);
a.count(0);//返回的是2
    
(9)lower_bound() / upper_bound()
lower_bound(x);//返回的是大于等于X的最小的数的迭代器
upper_bound(x);//返回的是大于X的最小的数的迭代器
//就是有没有等于的区别

(10)begin() / en()

a.begin()/ a.end();//返回迭代器支持++ --操作

3.map容器

(1)定义

键值对映射map是由键key和值value构成的一对单元,其中key value可以是任意的数据类型。map通过建立一颗红黑树(平衡二叉树)来实现对数据自动排序的功能,从而达到高效查询和检索的目的。

包含在map头文件中,搭配algorithmusing namespace std一起使用

其中key只能在map中出现一次

map<int, int> mp1; //定义一个int->int的映射
map<string, int> mp2; //定义一个string->int的映射
// 注意:map支持的字符串为string类型,而不是char*
map会自动初始化的,int型为0char型为'/0'
(2)插入元素insert()
map<char, int> mp; // 创建一个字符char->整形int的映射
// 方式一:利用pair构建一个映射单元,用insert函数插入pair
pair<char, int> p; // or pair<char, int> p('a', 0);
p1.first = 'a';
p1.second = 0;
mp.insert(p);
// 方式二:用数组下标的方式插入数据:
mp['b'] = 0
// 也可以直接使用该方式访问元素
cout<<mp['b'];

(3)删除元素

通过erase()方法来删除元素

// 方式一:通过key键删除,方便实用
mp.erase('a');
// 方式二:通过迭代器指针的方式删除
map<char, int>::iterator it=mp.begin();
mp.erase(it);
(4)查询关键字find()

在键值对映射中查找一个元素通过find()方法完成,若存在则返回一个迭代器指向元素所在的位置,否则返回键值对映射的尾地址:

map<char, int>::iterator it = mp.find('a');//可以通用auto代替
if(it==mp.end())cout<<"cannot found";
else cout<<it->first<<" "<<it->second;
// first为键,second为值
(5)遍历
for(map<char, int>::iterator it=mp.begin();it!=mp.end();it++)
    cout<<it->first<<" "<<it->second;

//支持随机访问[],但是时间复杂度是O(logn)
(6)清空clear

清空通过调用clear()方法实现,清空后大小变为0

mp.clear();
(7)multimap

key可以出现多次,所以无法用[ ]来访问value

(8)删除erase()
erase(pair/迭代器);

(9)操作符

operator:== != < <= > >=
注意:对于==运算符, 只有键值对以及顺序完全相等才算成立。

4.list容器

【C++】list容器介绍及使用_c++ list容器-CSDN博客

list 容器,又称双向链表容器,即该容器的底层是以双向链表的形式实现的。这意味着,list 容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间中。

每个元素都配备了 2 个指针,分别指向它的前一个元素和后一个元素

第一个元素的前向指针总为 null,因为它前面没有元素;同样,尾部元素的后向指针也总为 null。

优势:即它可以在序列已知的任何位置快速插入或删除元素(时间复杂度为O(1))。并且在 list 容器中移动元素,也比其它容器的效率高。

(1)定义:
#include <list>
using namespace std;

list<int>mylist;

//创建一个包含 n 个元素的 list 容器:
std::list<int> values(10);

// 创建一个包含 n 个元素的 list 容器,并为每个元素指定初始值。例如:
std::list<int> values(10, 5);

//在已有 list 容器的情况下,通过拷贝该容器可以创建新的 list 容器。例如:
std::list<int> value1(10);
std::list<int> value2(value1);

//通过拷贝其他类型容器(或者普通数组)中指定区域内的元素,可以创建新的 list 容器。例如:
//拷贝普通数组,创建list容器
int a[] = { 1,2,3,4,5 };
std::list<int> values(a, a+5);
//拷贝其它类型的容器,创建 list 容器
std::array<int, 5>arr{ 11,12,13,14,15 };
std::list<int>values(arr.begin()+2, arr.end());//拷贝arr容器中的{13,14,15}

(2)成员函数
成员函数功能
begin()返回指向容器中第一个元素的双向迭代器。
end()返回指向容器中最后一个元素所在位置的下一个位置的双向迭代器。
rbegin()返回指向最后一个元素的反向双向迭代器。
rend()返回指向第一个元素所在位置前一个位置的反向双向迭代器。
cbegin()和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend()和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin()和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend()和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
empty()判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
size()返回当前容器实际包含的元素个数。
max_size()返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
front()返回第一个元素的引用。
back()返回最后一个元素的引用。
assign()用新元素替换容器中原有内容。
emplace_front()在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。
push_front()在容器头部插入一个元素。
pop_front()删除容器头部的一个元素。
emplace_back()在容器尾部直接生成一个元素。该函数和 push_back() 的功能相同,但效率更高。
push_back()在容器尾部插入一个元素。
pop_back()删除容器尾部的一个元素。
emplace()在容器中的指定位置插入元素。该函数和 insert() 功能相同,但效率更高。
insert()在容器中的指定位置插入元素。
erase()删除容器中一个或某区域内的元素。
swap()交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。
resize()调整容器的大小。
clear()删除容器存储的所有元素。
splice()将一个 list 容器中的元素插入到另一个容器的指定位置。
remove(val)删除容器中所有等于 val 的元素。
remove_if()删除容器中满足条件的元素。
unique()删除容器中相邻的重复元素,只保留一个。
merge()合并两个事先已排好序的 list 容器,并且合并之后的 list 容器依然是有序的。
sort()小->大sort(greater())大->小通过更改容器中元素的位置,将它们进行排序。
reverse()反转容器中元素的顺序。
std::list<double> values;
//对容器中的元素进行排序
values.sort();
//使用迭代器输出list容器中的元素
for (auto it = values.begin(); it != values.end(); ++it) {
  std::cout << *it << " ";
}
(3)运算符

std::list运算符函数
operator ==:判断两个list是否相等
operator <:判断两个list容器是否"前者小于后者"
operator !=:判断两个list容器是否不相等
operator <=:判断两个list容器是否"前者小于或等于后者"
operator >:依次类推
operator >=:依次类推

(4)去重
L1.sort();//这里的sort是list的,不是标准库中的sort
L1.unique();
for_each(L1.begin(),L1.end(),Print);

list的迭代器不支持±数字,只能++/–

因为其底层空间不连续

(5)插入/删除

如果要往一个位置进行插入,可以通过find函数返回位置进行

list<int> l1{ 1,2,3,4,5 };
l1.insert(find(l1.begin(), l1.end(), 3), 10);//任意位置插入
l1.erase(find(l1.begin(), l1.end(), 10), l1.end());//任意位置的删除

img

(6)swap
list<int> l1{ 1,2,3,4,5 };
list<int> l2{ 5,6,7,8,9 };
l1.swap(l2);

img

(7)resize

resize改变有效元素的个数,多的元素用第resize二个参数填充,如果没有给第二个参数,则默认用T()。

list<int> l1{ 0,1,2 };
l1.resize(5, 3);

img

(8)remove/remove_if

remove_if的参数是一个判断条件,可以是函数指针或者函数对象

list<int> l1{ 3,0,1,3,2,3 };
l1.remove(3);

img

//判断5的倍数
bool MultipleFive(int n)
{
	return 0 == n % 5;
}

void Test10()
{
	//此处传递函数指针
	list<int> l1{ 10,0,1,3,5,7,20 };
	l1.remove_if(MultipleFive);//注意,参数是函数指针/函数对象,也就是函数名
}

img

(9)迭代器失效的问题

list底层结构为带头结点的双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,其他迭代器不会受到影响。

erase导致的迭代器失效

img

改正方法

while (it != l1.end())
{
	//it=l1.erase(it);
    //it++;
    
    
	l1.erase(it++);
    //这里 l1.erase(it++)语句也能达到效果,因为后置++会将自增后的结果保存在临时变量中,而前置则不可以
}

resize导致的迭代器失效
resize减少有效元素个数也会导致迭代器失效:

list<int> l1{ 1,3,5,7,9 };
auto it = l1.end();
l1.resize(3);

上面这个程序中,reseze减少有效元素个数后,it指向的位置元素已经被删除,迭代器失效,如果再使用该迭代器,则会出错。

(10)容易超时问题

因为list无法直接访问地址,所以容易超时,所以可以建一个迭代器类型的数组,直接存储每个元素的位置,而不用每一次调用find()

#include<bits/stdc++.h>
using namespace std;

const int N=100010;

using Iter= list<int>::iterator;
list<int>mylist(N);
Iter pos[N];

mylist.push_front(1);
pos[1]=mylist.begin();//list存储,pos数组直接存储位置

//在k之前插入
pos[i]=mylist.insert(pos[k],i); //在k之前的位置插入数i,pos[i]记录i的位置


//在k之后插入
auto nextIter= next(pos[k]);//用next()函数来得到k点的下一个位置,就可以在K点的后面插入元素
pos[i]=mylist.insert(nextIter,i);//插入在nextIter插入i,pos[i]记录位置


//删除指定的元素
bool erased[100];//全局变量,默认为false,避免重复删除
cin>>m;
for(int x,i=1;i<=m;i++)
{
    cin>>x;
    if(!erased[x])
    {
        mylist.erase(pos[x]);

    }
    erased[x]=true;
}

5.迭代器的移动

(1)advace()

advance() 函数移动的是源迭代器

#include <iostream>     // std::cout
#include <iterator>     // std::advance
#include <vector>
using namespace std;
 
//创建一个 vector 容器
vector<int> myvector{ 1,2,3,4 };
//it为随机访问迭代器,其指向 myvector 容器中第一个元素
vector<int>::iterator it = myvector.begin();
//输出 it 迭代器指向的数据
cout << "移动前的 *it = " << *it << endl;
//借助 advance() 函数将 it 迭代器前进 2 个位置
advance(it, 2);
cout << "移动后的 *it = " << *it << endl;

img

advance() 函数没有任何返回值,其移动的是 it 迭代器本身。

(2)prev()

该函数可用来获取一个距离指定迭代器 n 个元素的迭代器。

//函数原型
template <class BidirectionalIterator>
    BidirectionalIterator prev (BidirectionalIterator it, typename iterator_traits<BidirectionalIterator>::difference_type n = 1);

it 为源迭代器,其类型只能为双向迭代器或者随机访问迭代器;n 为指定新迭代器距离 it 的距离,默认值为 1。该函数会返回一个距离 it 迭代器 n 个元素的新迭代器。

注意,当 n 为正数时,其返回的迭代器将位于 it 左侧;反之,当 n 为负数时,其返回的迭代器位于 it 右侧。

#include <iostream>     // cout
#include <iterator>     // next
#include <list>         // list
using namespace std;
 
//创建并初始化一个 list 容器
list<int> mylist{ 1,2,3,4,5 };
list<int>::iterator it = mylist.end();
//获取一个距离 it 迭代器 2 个元素的迭代器,由于 2 为正数,newit 位于 it 左侧
auto newit = prev(it, 2);
cout << "prev(it, 2) = " << *newit << endl;
 
//n为负数,newit 位于 it 右侧
it = mylist.begin();
newit = prev(it, -2);
cout << "prev(it, -2) = " << *newit;
//当 it 指向 mylist 容器最后一个元素之后的位置时,通过 prev(it, 2) 可以获得一个新迭代器 newit,其指向的是距离 it 左侧 2 个元素的位置(其存储的是元素 4);当 it 指向 mylist 容器中首个元素时,通过 prev(it, -2) 可以获得一个指向距离 it 右侧 2 个位置处的新迭代器。

img

注意,prev() 函数自身不会检验新迭代器的指向是否合理,需要我们自己来保证其合理性。

(3)next()
//函数原型
template <class ForwardIterator>
    ForwardIterator next (ForwardIterator it, typename iterator_traits<ForwardIterator>::difference_type n = 1);

it 为源迭代器,其类似可以为前向迭代器、双向迭代器以及随机访问迭代器;n 为指定新迭代器距离 it 的距离,默认值为 1。该函数会返回一个距离 it 迭代器 n 个元素的新迭代器。

it 为源迭代器,其类似可以为前向迭代器、双向迭代器以及随机访问迭代器;n 为指定新迭代器距离 it 的距离,默认值为 1。该函数会返回一个距离 it 迭代器 n 个元素的新迭代器

#include <iostream>     // std::cout
#include <iterator>     // std::next
#include <list>         // std::list
using namespace std;
 
//创建并初始化一个 list 容器
list<int> mylist{ 1,2,3,4,5 };
list<int>::iterator it = mylist.begin();
//获取一个距离 it 迭代器 2 个元素的迭代器,由于 2 为正数,newit 位于 it 右侧
auto newit = next(it, 2);
cout << "next(it, 2) = " << *newit << endl;
 
//n为负数,newit 位于 it 左侧
it = mylist.end();
newit = next(it, -2);
cout << "next(it, -2) = " << *newit;

img

和 prev() 函数恰好相反,当 n 值为 2 时,next(it, 2) 函数获得的新迭代器位于 it 迭代器的右侧,距离 2 个元素;反之,当 n 值为 -2 时,新迭代器位于 it 迭代器的左侧,距离 2 个元素。

注意,和 prev() 函数一样,next() 函数自身也不会检查新迭代器指向的有效性,需要我们自己来保证。

6.queue

队列queue容器的C++标准头文件为queue,提供的基础操作及实例如下:

  • empty判断队列是否为空,若空则返回true,否则返回false

  • size返回当前队列的大小,即队列里元素的个数;

  • front返回队首元素,back返回队尾元素;

  • push将元素添加到队尾;

  • pop移除队首元素。

  • 没有clear()函数,如果想清空可以

  • q=queue<int>();
    
  • q.count(t);查询次数

7.priority_queue/优先队列

(1)定义

优先队列中的元素被赋予优先级的概念:当访问或删除元素时,具有最高优先级的元素最先被操作,也就是说优先队列具有最高级先出first in, largest out的特性,通常具体采用堆的数据结构来实现。

优先队列priority_queue容器的C++标准头文件为queue

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

priority_queue<int>mypq;

(2)基础操作
  • empty判断优先队列是否为空,若空则返回true,否则返回false
  • size返回当前优先队列的大小,即优先队列里元素的个数;
  • top返回优先队列的最高优先级的元素;
  • push将元素添加到优先队列,STL中的优先队列会根据元素的优先级自动调整;
  • pop移除优先队列的队首元素。
(3)重载

创建一个优先队列,需要制定数据类型的优先级比较方式,基础的数据类型会有默认的优先级比较方式,例如整型数据默认的比较方式为数值越大优先级越高。

STL也提供了通过重载结构体的方式自定义优先级也可以插入元素的负数

// 方式一
priority_queue<int,vector<int>, greater<int> > que1; // x小的优先级高
// 方式二
struct comp
{
        bool operator()(int x,int y)
        {
            return x>y;   //重载()的方式,x小的优先级高
        }
    };
priority_queue<int, vector<int>, comp> que2; // 从小到大的优先队列
// 方式三
struct node {
  int x, y;
  friend bool operator < (node a, node b) {
    return a.x > b.x;    //结构体类型的方式,x小的优先级高
  }
};
priority_queue<node> que3;

8.pair

(1)定义
pair<int,string> p;
(2)first / second
p.first; //为a的第一个元素
p.second;//为a的第二个元素
(3)支持比较运算

按字典序,ASCII码

以first为第一关键字,以second为第二关键字

(3)初始化
p= make_pair(10,"abc");
p= {20,"abc"};//C++11以后支持
(4)存储三个
pair<int,pair<int,int> > p;

9.stack

/*
push():向栈顶插入元素
pop():弹出栈顶元素
top():返回栈顶元素
size():返回栈的大小
empty():判断是否是空的
*/

10.deque

双端队列,相当于加强版的vector,但是速度比较慢

/*
size()
clear()
front()
back()
push_back():向队尾插入元素
pop_back():从队尾删除元素
push_front():从队首插入元素
pop_front():从队尾删除元素
[]:支持随机访问
begin()/end():返回迭代器
*/

11.unordered_set / unordered_map / unordered_multiset / unordered_mutilmap

一个内部为无序的set / map

与上面的类似,并且增删改查的时间复杂度为O(1),但是不支持lower_bound()和upper_bound,以及迭代器的++ / –

unordered_map、unordered_set是哈希表,它的查找、插入和删除操作的平均时间复杂度都是常数级别的,即O(1)find函数和count函数来说,它们的时间复杂度也都是O(1),最坏是O(N)。

12.bitset

压位,将一个字节压在一位上,相当于只有原来空间的 1 8 \frac{1}{8} 81 ,100MB可以压成12MB,节省空间

用于表示二进制位序列,用于表示二进制位序列,尤其适用于位运算操作。每个位都只能是 0 或 1。这个固定长度在创建对象时指定,并且不能在运行时更改。类似于整数类型

(1)定义
#include <bitset>

bitset<N> bitset1; // 创建一个长度为 N 的 bitset,所有位都被初始化为 0
bitset<N> bitset2(value); // 使用二进制整数 value 初始化一个长度为 N 的 bitset
bitset<N> bitset3(string); // 使用二进制字符串 string 初始化一个长度为 N 的 bitset
bitset<N> bitset4(bitset); // 使用另一个 bitset 初始化一个长度为 N 的 bitset
 


bitset<4>a1;//长度为4,默认以0填充
bitset<8>a2;//长度为8,将12以二进制保存,前面用0补充


string s = "100101";
bitset<10>a3(s);//长度为10,前面用0补充

//实验检测,char在普通环境不能直接赋值给bitset
//要开c++11,针不戳
char s2[] = "10101";
bitset<13>a4(s2);//长度为13,前面用0补充
//所以这玩意noip上不能用……

cout<<a1<<endl;//0000
cout<<a2<<endl;//00001100
cout<<a3<<endl;//0000100101
cout<<a4<<endl;//0000000010101

(2)常用操作
size() 返回 std::bitset 的长度
count() 返回 std::bitset 中值为 1 的位的数量
any() 返回 std::bitset 中是否存在值为 1 的位
none() 返回 std::bitset 中是否所有位都是 0
all() 返回 std::bitset 中是否所有位都是 1
test(pos) 返回 std::bitset 中位于 pos 位置的值
set(pos) 将 std::bitset 中位于 pos 位置的值设为 1
reset(pos) 将 std::bitset 中位于 pos 位置的值设为 0
flip(pos) 将 std::bitset 中位于 pos 位置的值取反
to_ulong() 返回 std::bitset 转换成的无符号整数值
to_ullong() 返回 std::bitset 转换成的无符号长整数值
to_string() 将其转换成二进制字符串

可以使用下标访问、迭代器等方式访问其元素。此外,它还可以通过位集合(bitset set operations)进行集合运算,如并集、交集、补集等,可以使用 std::bitset 的成员函数 set()reset()flip() 进行相应的集合操作

(3)支持位运算
bitset<4> bitset3 = bitset1 & bitset2; // 按位与运算
bitset<4> bitset4 = bitset1 | bitset2; // 按位或运算
bitset<4> bitset5 = bitset1 ^ bitset2; // 按位异或运算
bitset<4> bitset6 = ~bitset;//取反运算

bitset<4> bitset1("0101");
bitset<4> bitset2 = bitset1 << 2; // 左移 2 位,结果为 "010100"
bitset<4> bitset3 = bitset1 >> 1; // 右移 1 位,结果为 "0010"
    
 

七、算法的时间复杂度

一般题目会限制时间为1s,在1s内计算机最多进行5*10^8次运算。

由于时间复杂度N的前面还会存在常数,因此以下的最大数据规模除以10或除以2都是很保险的,不会发生超时现象。

O(N)数据规模最多为10^8
O(N*logN)数据规模最多为10^7
O(N^sqrt(N))数据规模最多为10^6
O(N^2)数据规模最多为10^4
O(N^3)数据规模最多为600
O(2^N)数据规模最多为25
O(N!)数据规模最多为11

y总经验分享

20210127112853383

八、算法基础

1.二分

两种二分的区别:[[二分]]

浅谈两种二分模板 - Frank_Ou - 博客园 (cnblogs.com)

#include<bits/stdc++.h>
using namespace std;

int x[50];
int n,k;

//right=size
//相当于查找左边界,下届
//如果是找最小就用这个
//它会返回第一个大于等于查找元素的下标,等同于lower_bound()
void boud_begin(int x[],int left,int right)
{
	while(left<right)
	{
		int mid= right+left >> 1;
		if(x[mid]>=k)
		{
			right=mid;
		}
		else
		{     
			left=mid+1;
		}
		
	}//while循环结束的时候right==left 
	if(x[left]!=k)
	{
		cout<<"不存在";
	}
	else
	{
		cout<<"存在 "<<left;
	}
	
}
	
	
//第二种二分	
//返回的是查找元素,最后一个元素的下标
//相当于查找右边界,上界
//如果是找最大就用这个
//当查找元素存在时,会返回等于查找数字的元素的最大下标。
//如果查找元素不存在,会返回小于查找数字的最大元素的最大下标
//类似upper_bound() - 1
void bound_end(int x[],int left ,int right)
while(left<right)
{
    int mid=right+left+1>>1;
    if(x[mid]<=k)
    {
        left=mid;
    }
    else
    {
        right=mid-1;
    }
}
cout<<left 

}

//浮点数二分 
//浮点数二分就不用考虑R,L+1 和 -1的问题了
void bond_double_find()
{
	double x;
	cin>>x;
	double left=0,right=x;
	while(right-l> 1e-6)//1e-6还是1e-8取决于保留的小数的位数,一般是比保留的位数多两位
	{
		double mid=(l+right)/2;
		if(mid*mid>=x)
		{
			right=mid;
		}
		else
		{
			left=mid;
		}
			
	}
	cout<<left;
}

int main()
{ 
	cin>>n>>k;
	for(int i=0;i<n;i++)
	{
		cin>>x[i];
	}
	
	boud_int_find(x,0,n-1);
}

2.快排

#include <bits/stdc++.h>
using namespace std;

const int N = 100;
int x[N], k;

void q_sort(int x[],int l, int r)
{
    if (l >= r)
        return;
    int i = l - 1, j = r + 1, mid = x[l + r >> 1];
    while(i<j)
    {
        do 
            i++;
        while (x[i] < mid);
        do
            j--;
        while (x[j] > mid);
        if (i < j) swap(x[i], x[j]);
    }
    q_sort(x,l, j);
    q_sort(x,j + 1, r);
}
int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> x[i];
    }
    q_sort(x,0, n - 1);
    for (int i = 0; i < n;i++)
    {
        cout << x[i];
    }
}

3.高精度加法

一般要到 1e6的程度;
大整数的存储;

#include<bits/stdc++.h>
#include<vector>
using namespace std;
vector<int> add(vector<int>&A,vector<int>&B)//用引用型会更快,不用copy
{
	vector<int>c;
	int t=0;//t是进位
	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);//一定是push_back不能是[]
		t/=10;//进位输出
	}
	if(t)//如果最高的进位输出不为0的话
	{
		c.push_back(1);//就进一位
	}
	return c;//返回C
	
}
int main()
{
	string a,b;
	vector<int>A,B;
	cin>>a>>b;
	for(int i=a.size()-1;i>=0;i--)//a的长度-1,不是A.size()-1
	{
		A.push_back(a[i]-'0');//不要忘记-'0',否则就成了字符了
	}
	for(int i=a.size()-1;i>=0;i--)//b的长度-1,不是B.size()-1
	{
		B.push_back(b[i]-'0');
	}
	vector<int> c = add(A,B);
	for(int i=c.size()-1;i>=0;i--)//长度-1
	{
		printf("%d",c[i]);
	}
} 
    
    

4.高精度减法

1 push_back()back()
c.push_back(X) 将元素X加入到c容器的最后一位。
c.back() 返回c容器的最后一个元素的值,并不是该元素的地址。

2 push_back()pop_back( )
push_back() 在Vector最后添加一个元素(参数为要插入的值);
删除Vector容器中的最后一个元素;

#include<iostream>
using namespace std;

bool cmp(vector<int>&A,vector<int>&B)
{
	if(A.size()!= B.size())//如果位数不同 
	{
		return A.size()>B.size();//那么如果A大则返回True
		//如果B大,则返回False 
	}
	for(int i=A.size()-1;i>=0;i--)//此时A.size()==B.size() 
	{
		if(A[i]!=B[i])//从最高位开始比较 
		{
			return A[i]>B[i];//那么如果A大则返回True
		//如果B大,则返回False 
		}
	}
	return true;//此时是A==B ,返回True 
 } 

vector<int> sub(vector<int>&A,vector<int>&B)
{
	vector<int>C;
	for(int i=0,t=0;i<A.size();i++)
	{
		t=A[i]-t;
		if(i<B.size())//因为B.size()<=A.size,所以要判断B是否存在
		{
			t-=B[i];
		}
		C.push_back((t+10)%10);
		if(t<0)//判断t是否借位
		{
			t=1;
		}
		else 
		{
			t=0;
		}
	}
	
	while(C.size()>1&&C.back()==0)//去掉前导0,千万不要忘记
	{
		C.pop_back();//不断地删除前导0
	}
	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');
	}
	
	if(cmp(A,B))
	{
		 c=sub(A,B);
	}
	else
	{
		 c=sub(B,A);//如果B>A,那么要加负号
		cout<<'-'; 
	}
	
		for(int i=c.size()-1;i>=0;i--)
	  {
		printf("%d",c[i]);
	  }
	
} 

5.高精度乘法

#include<bits/stdc++.h>
using namespace std;


vector<int> mul(vector<int>&A,int b)
{
	vector<int> C;
	int t;
	for(int i=0;i<A.size()||t;i++)
	{
		if(i<A.size())
		{
			t+=A[i]*b;
		}
		
		C.push_back(t%10);
		t/=10;
	}
	return C;
	 
}
int main()
{
	int b;
	string a;
	cin>>a>>b;
	vector<int>A;
	for(int i=a.size()-1;i>=0;i--)
	{
		A.push_back(a[i]-'0');
	}
	vector<int> C= mul(A,b);
	for(int i=C.size()-1;i>=0;i--)
	{
		printf("%d",C[i]);
	}
}

6.高精度除法

#include<bits/stdc++.h>
using namespace std;

//高精度除法 
//  A/B,商是c,余数是r 
vector<int> div(vector<int>&A,int b,int &r) 
{
	r=0;
	vector<int>C;
    //C[0]存的最低位,C[size]存的最高位 
	
    
    //除法是从最高位开始的 
	for(int i=A.size()-1;i>=0;i--)//所以要从A.size()开始,不是从0开始 
	{
		r=r*10+A[i];
		C.push_back(r/b);
		r%=b;
	}
	
	reverse(C.begin(),C.end());//,C[i]是倒序输出,所以反转C使得C[0]为最低位
	while(C.size()>1&&C.back()==0)//删除前导0 
	{
		C.pop_back();
	} 
	return C;
}

int main()
{
	string a;
	int b,r;
	vector<int>A;
	cin>>a>>b;
	for(int i=a.size()-1;i>=0;i--)
	{
		A.push_back(a[i]-'0');
	}
	
	vector<int>C= div(A,b,r);
	
	for(int i=C.size()-1;i>=0;i--)
	{
		printf("%d",C[i]);
	}
	
	cout<<endl<<r<<endl;
	return 0;
	
}

  

7.前缀和

[[前缀和]]

#include<bits/stdc++.h>
using namespace std;

//前缀和
//si=a1+a2+a3+a4..... 
//S0=0,从S1开始 

const int N=100010;
int n,m;
int a[N],s[N]={0};

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)//下标从1开始 
	{
		scanf("%d",&a[i]);
		s[i]=s[i-1]+a[i];//对前缀和进行初始化 
	}
	
	while(m--)//区间和的计算 
	{
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d\n",s[r]-s[l-1]);
		
	} 
	
	return 0;
	
	
}

8.二维前缀和

#include<iostream>
using namespace std;

const int N=1010;
int n,m,q;
int a[N][N],s[N][N];
 

int main()
{
	cin>>n>>m>>q; 
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			scanf("%d",&a[i][j]);//下标从1开始 
		}
	}
	
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)//下标从1开始 
		{
			s[i][j]=s[i][j-1]+s[i-1][j]-s[i-1][j-1]+a[i][j];
			//求前缀和 
		}
	}
	
	while(q--)//开始查询 
	{
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1];
		//算子矩阵的和 
		
	}
} 

image-20240331223829590

9.差分

/*一维差分是指给定一个长度为n的序列a,要求支持操作inset(l,r,c)表示对a[l]~a[r]区间上的每一个值都加上或减去常数c,并求修改后的序列a。
将对a数组任意区间的同一操作优化到O(1)。*/
#include<iostream>
using namespace std;

const int N=100010;
int n,m;
int a[N],b[N];
//假定a[i]初始全为0 
//数组一定定义在外面,这样可以省略传地址的过程 

void insert(int l,int r,int c)
{
	b[l]+=c;
	b[r+1]-=c;
}
int main()
{
	scanf("%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		insert(i,i,a[i]);
	} 
	while(m--)
	{
		int l,r,c;
		scanf("%d%%d%d",&l,&r,&c);
		insert(l,r,c);
	}
	for(int i=1;i<=n;i++)
	{
		b[i]+=b[i-1];
	}
	for(int i=1;i<=n;i++)
	{
		printf("%d",b[i]);
	}
}

差分有两种表现形式

/*第一种差分:从前往后*/
void insert(int l,int r,int c)
{
	b[l]+=c;
	b[r+1]-=c;
}
/*第二种差分:从后往前*/
for(int i=n+1;i;i--)
{
    b[i]-=b[i-1];
}//注意从n+1开始

10.二维差分

#include<iostream>
using namespace std;

const int N=1010;
int n,m,q;
int a[N][N],b[N][N];
//a[][]为原数组,b[][]差分数组 

//a数组是b数组的前缀和


void insert(int x1,int y1,int x2,int y2,int c)
{
	b[x1][y1]+=c;
	b[x2+1][y1]-=c;
	b[x1][y2+1]-=c;
	b[x2+1][y2+1]+=c;
}
int main()
{
	cin>>n>>m>>q;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=m;j++)
		{
			cin>>a[i][j];
			insert(i,j,i,j,a[i][j]);//将a[i][j]插到b[i][j]中 
			
		}
	}
	
	
	while(m--)
	{
		int x1,y1,x2,y2,c;
		cin>>x1>>y1>>x2>>y2>>c;
		insert(x1,y1,x2,y2,c);//开始插入C 
	}
	
	for(int i=1;i<=n;i++)
	{
		for(j=1;j<=m;j++)
		{
			b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];//对b数组自己求前缀和 
			//所以递推公式有+=,相当于把二位前缀和的a[i][j]换成b[i][j] 
		}
	}
}
const int N = 100010;
int n; //n数组长度
//定义两个一维整形数组 a为原数组,b为差分数组
int a[N],b[N];  
 
//根据定义可知
b[i] = a[i] - a[i-1];
//稍微具体
b[1] = a[1];
b[2] = a[2] - a[1];
b[3] = a[3] - a[2];
...
b[i] = a[i] - a[i-1];
 
//转化一下,求数组b的前缀和,根据上面公式可得
  b[1]+b[2]+b[3]+...+b[i]
= a[1]+(a[2]-a[1])+(a[3]-a[2])+...+(a[i]-a[i-1])
= a[i]
 
//由此可知,原序列为差分序列的前缀和序列
a[i] = b[1]+b[2]+b[3]+...+b[i];
 

image-20240302173135316

差分和前缀和是相互逆反的东西,当对差分数组进行操作之后,在对差分数组进行求和,那么原数组的每一个都会改变,因为差分数组是由原数组的每一项相减得到的,再加回去,得到的仍然是原数组。

11.双指针

[[双指针]]

#include<iostraem>
#include<algorithm> 
using namespace std;
//核心思想就是,一般从暴力开始优化,将O(N*2)优化到O(N) 
/*
for(int i=0;i<n;i++)
	{
		for(int j=0;j<n && check(i,j);j++)
		{
			//每一道题的具体逻辑 
		}
	}
*/


const int N=100010;
int n;
int a[N][N],b[N][N];

int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		cin>>a[i];
	}
	
	int res=0;//就是长度 
	for(int i=0,j=0;i<n;i++)
	{
		s[a[i]]++;//找不同数字的个数 
		while(s[a[i]]>1)
		{
			s[a[j]]--;
			j++;
		}
		res=max(res,i-j+1);
	}
	cout<<res<<endl;
	 

}

12.位运算

[[位运算]]

n的二进制表示中第K位是几

①先把第K位移到最后一位,n>>k

②看个位是几,x&1

n>>k&1

#include<iostream>
using namespace std;
int n=10; 
for(int k=3;k>=0;k--)//K要从大到小
{
    cout<< (n >> k & 1);
}
//可以直接输出n的二进制是多少

//这里是K>=0,所以 
//注意K的大小,如果K==4,那么输出的是五位 
//K==3,输出的是4位 
//K+1不能比n的位数小 

13. lowbit(x)

作用:返回X的二进制形式的最后一位1

x=1010;
lowbit(x);//结果为10
x=101000;
lowbit(x);//结果为1000
	
实现:
    x&-x == x&(~x+1)//意味对x取反+1
    
#include<iostream>
using namespace std;
//可以输出X里有多少个1 
//注:x是十进制数 
int lowbit(int x)
{
	return x&-x;
 } 
int main()
{
	int n;
	cin>>n;
	while(n--)
	{
		int x;
		cin>>x;
		
		int res=0;
		while(x)//X不为0 
		{
			x-=lowbit(x);//每次减去X的最后一位1 
			res++;
			
		}
		cout<<res<<" ";
	}
}    
    
    

14.整数离散化

[[离散化]]

(1)二分:

①去重:

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

unique(bgein,end);//只是把重复的元素放到后面,实际上仍然存在于数组中
erase(unique(begin,end));//则是真正的去重,会把重复的元素从数组中删除,只返回没有重复元素的数组的尾指针

②二分:

//二分求出X对应的离散化的值
//b
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,是为了从1开始映射,1,2,3,4,5 。。。
    //如果不+1,则是从0开始映射
}

(2)映射:

//离散化函数,对数组e进行离散化
void desperse(int e[])
{
    for(int i=0;i<n;i++)
    {
        p[i]=i;
    }
    sort(p,p+n,[&](int x,int y){
        return e[x]<e[y];
    });
    // lambda表达式,按照e数组的大小对p进行排序,为按引用传参
    
    for(int i=0;i<n;i++)
    {
        e[p[i]]=i;
    }
}
 /*
    p数组根据e数组的值排序之后,p数组的值就变为e数组的值所对应的下标的排序
    (相当于是p数组是e数组的下标,对下标进行排序)
    然后再根据p数组修改原数组

    e:2 3 1 4
    p:0 1 2 3

    经过sort之后
    e:2 3 1 4 e[0]=2 e[1]=3 e[2]=1 e[3]=4
    p:2 0 1 3

    e被p修改
    e:1 2 0 3
    */
    

15.有交集的区间合并

一对一的STL容器有map容器,但是map容器中的按键值排序和不允许由重复的元素

vector<pair<int,int> >来实现一对一,但其没有排序可以允许有重复的元素

对于存在交集的两个区间进行合并,得到并集

#include<iostream>
#include<algorithm>
#include<vector>

using namespace std;

const int N=100000;
typedef pair<int,int>PII;
int n;
vector<PII>segs;

void merge(vector<PII>&segs)
{
	vector<PII>res;
	sort(segs.begin(),segs.end());
	//C++中优先以左端点进行排序,然后再以右端点进行排序 
	int first=-2e9,end=-2e9;
	for(auto seg:segs)
	{
		if(end<seg.first)//没有交集 
		{
			if(first!=-2e9)
			{
				res.push_back({first,end});//把区间存在res中 
			}
			first=seg.first;//更新一下左端点 
			end=seg.second;//更新右端点 
		}
		else
		{
			end=max(end,seg.second);
			//有交集,那么把右端点更新成较大的右端点,实现合并 
		}
	}
	if(first!=-2e9)
	{
		res.push_back({first,end});//把最后一个区间存进去 
	}
	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;//输出个数 
	
 } 

16.单链表

#include<bits/stdc++.h>
using namespace std;

const int N=100010;

//head表示头结点的下标
//data[i]表示结点i的值,代表数据域 
//next[i]表示结点i的next指针是多少,代表指针域 
//index存储当前已经用到了那个点,也代表指针 
//下标从0开始 

int head,data[N],ne[N],index;

void init()
{
	head=-1;
	index=0;
} 
//将X插到头结点 
void add_to_head(int x)
{
	data[index]=x;//将X存下来 
	ne[index]=head;//将head指向的点的地址给index点 
	head=index;//令head指向index 
	index++;
}

//将X插入到下标为K的点的后面 
void add_k(int x,int k) 
{
	data[index]=x;
	ne[index]=ne[k];
	ne[k]=index; 
	index++; 
}
//将下标是K的点的后面的点删掉 
void remove(int k) 
{
	ne[k]=ne[ne[k]];//连着指向两次 
}

int main()
{
	int x,k;
	cin>>x>>k;
	init();
	add_to_head(x);
	add_k(x,k);
	remove(k);
	//删除第一个结点
	if(!k)
	{
		head=ne[head];
	 } 
	//遍历 
	for(int i=head;i != -1;i=ne[i])
	{
		cout<<data[i]<<" ";
	}
	cout<<endl;
}

17.避免重复访问

bool first=true;
	for(int x:mylist)
	{
		if(!first)
		{
			cout<<" ";
		}
		first=false;
		cout<<x;
	}

18.约瑟夫环

有n只猴子,按顺时针方向围成一圈选大王(编号从1到n),从第1号开始报数,一直数到m,数到m的猴子退出圈外,剩下的猴子再接着从1 开始报数。就这样,直到圈内只剩下一只猴子时,这个猴子就是猴王,编程求输入n,m后,输出最后猴王的编号。

求最后活下来的那个人

int cir(int n,int m)
{
	int p=0;
	for(int i=2;i<=n;i++)
	{
		p=(p+m)%i;
        cout<<p<<" ";
	}
	return p+1;
}

求每一次出列的人

#include<bits/stdc++.h>
using namespace std;

const int N=100010;
int n,m,s;
bool visit[N];

int main()
{
	cin>>n>>m;
	for(int i=0;i<n;i++)//总共出队n次 
	{
		for(int j=0;j<m;j++)
		{
			if(++s>n)//序列从1开始,所以++s,不是s++ 
			{
				s=1;
			}
			if(visit[s])
			{
				j--;
			}
		}
		cout<<s<<" ";
		visit[s]=true;
	}
	return 0;
 } 

19.朴素匹配

const int N=10010,M=10010;
int s[N],p[M];
for(int i=1;i<=n;i++)//从1开始
{
    bool flag=true;
    for(int j=1;j<=m;j++)
    {
        if(s[i]!=p[j])
        {
            flag=false;
            break;
        }
    }
    if(flag==true)
    {
        cout<<"匹配成功,起始位置为:"<<i-j;//根据题目来进行调整输出
    }
}

20.KMP

复杂度为O(N)

const int N=10010,M=10010;
int n,m;
char p[N],s[M];//p为模板串,S为模式串
int ne[N];

int main()
{
    cin>>n>p+1>>m>>s+1;//从p+1、m+1开始输入
     
    //求next数组
    //求最大的前缀后缀
    for(int i=2,j=0;i<=n;i++)
        //从2开始寻找,因为1的时候肯定没有
     {
         while(j &&p[i]!=p[j+1])
             j=ne[j];
        //j要>0,当j==0的时候,表示退无可退,如果没有j>0,那么就会陷入死循环。
       
         if(p[i]==p[j+1])//如果相等j++,一直到不相等
             j++;
         ne[i]==j;//记录此时的最大相等的前缀/后缀长度
     }
    
    
    //匹配的过程
    for(int i=1,j=0;i<=m;i++)//i从1开始,j从0开始
    {
        while(j&&s[i]!=p[j+1])
        {
            j=ne[j];
        }
        //比较的是s[i]和p[j+1],
        //当不相等的时候,j跳转的是ne[j]
        
        if(s[i]==p[j+1])
            j++;
        //相等的时候,j++,继续往前走
        
        if(j==n)//当j==n的时候,j已经无法再走了,表示匹配成功
        {
            //匹配成功
            cout<<i-n+1;//如果下标从0开始就 i-n
            j=ne[j];//j跳到ne[j],开始往后匹配,寻找另外能否匹配成功
        }
    }
    return 0;
    
}

image-20231019205222897

21.Tire树

高效的存储和查找字符串集合的数据结构

字符串的个数不会很多,要么全是小写字母,要么全是大写字母,要么全是数字

把所有串的结尾标记一下

注意:

  1. 对于大量数据时,数组的初始化不要用memset,以及用scanf()和printf()
  2. 如果区分大小写,以及存在字母的话
int son[123];//小写z的ASCII值为122,123='z'+1;
int u= str[i]-'0';
const int N=10010;

int son[N][26];//存的是所有节点的儿子结点
//一共有26个字母(区分大小写,这里只是一种情况)
//如果大小写全由那么就 int son[N][52];
int count[N];//存的是以当前结点结尾的单词有多少个
int index;//存储的是当前的下标,root的index==0,root是空结点
char str[N];

//存储/插入操作
void insert(char str[])
{
    int p=0;
    for(int i=0;str[i];i++)//从根节点开始
    {
        int u=str[i]-'a';//当前子节点的编号,映射一下
        if(!son[p][u])//如果p结点的u儿子不存在的话,就创建出来
            son[p][u]== ++index;//index是字符的编号
        p=son[p][u];//更新一下p
    }
    count[p]++;
    //以p这个点所代表的字母结尾的单词多了一个
    
}
//查询操作
int query(char str[])
{
    int p=0;
    for(int i=0;str[i];i++)//从根节点开始
    {
        int u=str[i]-'a';//映射一下
        if(!son[p][u])
            return 0;//不存在,直接返回0
        p=son[p][u];
    }
    return count[p];//返回以p结尾的单词数量
}
int main()
{
    int n;//串的个数
    cin>>n;
    while(n--)
    {
        char op[2];
        cin>>op>>str;
        if(op[0]=='I')
            insert(str);
        else
            cout<<query(str); 
    }
    
    
}

image-20231019220609034

前缀Tire树

//存储/插入操作
void insert(char str[])
{
    int p=0;
    for(int i=0;str[i];i++)//从根节点开始
    {
        int u=str[i]-'a';//当前子节点的编号,映射一下
        if(!son[p][u])//如果p结点的u儿子不存在的话,就创建出来
            son[p][u]== ++index;//index是字符的编号
        p=son[p][u];//更新一下p
        count[p]++;//直接记录每一个单词中每个字母的个数
    }
    
}

//query没有任何变化

22.并查集

快速的处理:

1.将两个集合合并

2.询问两个元素是否在一个集合当中

时间复杂度:近似O(1)

基本思路:

image-20231022205649812

//每一个集合用一个树来表示,根节点的编号就是这个集合的编号
//每个结点存储他的父节点,p[x]表示x的父节点

//问题一查询:分别求x,y的树的编号,如果一样,就是在一个集合中,反之不在
find(a)==find(b);

//问题二合并:px是x的集合编号,py是y的集合编号。合并:p[x]=y;将y树作为x树的儿子
p[find(a)]=find(b);


并查集的优化:路径压缩

//如果找到x的根节点,就把这条路径上的所有点都变为根节点的儿子,直接指向根节点,相当于只遍历1遍
int find(int x)
{
    if(p[x]!=x)
    {
        p[x]=find(x);
    }
    return p[x];
}
const int N=100010;

int p[N];//每个元素的父节点
int n,m;

int find(int x)//返回X所在集合的编号/祖宗节点+路径压缩
{
    if(p[x]!=x)//如果x不是根节点的话,继续向上找,直到找到根节点
        p[x]=find(p[x]);
    return p[x];
    //相当于p[[[[[[[[[x]]]]]]]]]==x
}
//完成这一步后,之后的find(x),时间复杂度就是O(1)了。
//find(a)和p[a]就是一样的了  ?
//不一样的,因为一旦有别的集合加入进来,find(a)要更新的,p[a]不会更新

int main()
{
    cin>>n>>m;//n是个数,m是操作数
    //初始化
    for(int i=1;i<=n;i++)
    {
        p[i]=i;//每个点的根节点是自己
    }
    while(m--)
    {
        char op[2];
        //用scanf读入一个字母时,用字符串数组的话,scanf()会自动忽略掉一些额外的空格和回车
        int a,b;
        cin>>op>>a>>b;
        
        if(op[0]=='M')//合并操作
            p[find(a)]=find(b);
        //b的根节点成为,a的根节点的父亲结点,即a集合成为b集合的子集合
        
        else//查询操作
        {
            if(find(a)==find(b))
                cout<<"YES";
            else
                cout<<"No";
        }
    }
}

并查集的优化:按秩合并

例题:

image-20231022214014423

const int N=100010;
int p[N],size[N];//size是每一个集合中点的数量

int find(int x)
{
    if(p[x]!=x)
        p[x]=find(p[x]);
    return p[x];
}

int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)
    {
        p[i]=i;
        size[i]=1;//每个集合的个数初始化为1
    }
    if(op[0]=='C')
    {
        cin>>a>>b;
        if(find(a)==find(b))
            continue;//特判一下,if a,b已经在一个集合中了,跳过
        size[find(b)]+=size[find(a)];
        p[find(a)]=find(b);
    }
    else if(op[1]=='1')
    {
        if(find(a)==find(b))
            cout<<"YES";
        else
            cout<<"NO";
    }
    else
    {
        cin>>a;
        cout<<size[find(a)];
    }
}

23.手写堆

堆是一个完全二叉树

down/up()的时间复杂度为O(logn);

求最小值为O(1);

功能:

image-20231023184528474

image-20231023183713340

//建堆的时间复杂度为O(N);
//O(N)的建堆,从n/2到1
for(int i=n/2;i;i--)
{
    down(i);
}

image-20231023190941424

//堆的左儿子的编号为2x,右儿子的编号为2x+1
//下标从1开始,如果从0开始的话,左儿子还是0,右儿子变成-1,会冲突
const int N=10010;
int n,m;
int heap[N],size,ph[N],hp[N];
//ph[k]存的是第k个插入的数的下标,在树中是哪一个点
//hp[k]存的是堆里面的k点是第几个插入点
//建立映射
//这一步是在heap_swap完之后执行的,
//ph[i]=k,hp[k]=j

void heap_swap(int a,int b)
    //堆中交换两个点
{
    swap(ph[hp[a]],ph[hp[b]]);//交换下标
    swap(hp[a],hp[b]);//
    swap(heap[a],heap[b]);//交换值
}
void down(int u)
{
    int t=u;//t表示左右儿子以及这个点,三个点中最小值的编号
    if(u*2<=size && heap[u*2]<heap[t])//左儿子存在,并且左儿子的值小于t点的值
    {
        t=u*2;//t变成左儿子
    }
    if(u*2+1<=size && heap[u*2+1]<heap[t])//右儿子存在,且右儿子的值小于t的值
    {
        t=u*2+1;//t变成右儿子
    }
    if(u!=t)//如果u!=t说明根节点不是最小的,
    {
        //swap(heap[u],heap[t]);//没有4和5时
        heap_swap(u,t);
        down(t);
    }
}

void up(int u)
{
    while(u/2 && heap[u/2]>heap[u])//找到父节点,并且父节点的值大于该点的值
    {
        //swap(heap[u/2],heap[u]);//没有4和5时
        heap_swap(u/2,u);//交换
        u/=2;//u更新成父节点,继续向上找
    }
}

int main()
{
    int n,m=0;
    scanf("%d",&n);
    while(n--)
    {
        char op[10];
        int k,x;
        scanf("%s",op);
        if(!strcmp(op,"I"))
        {
            scanf("%d",&x);
            size++;
            m++;
            ph[m]=size;
            hp[size]=m;
            heap[size]=x;
            up(size);
        }
        else if(!strcmp(op,"PM"))
        {
            cout<<heap[1]<<endl;
        }
        else if(!strcmp(op,"DM"))
        {
            heap_swap(1,size);
            size--;
            down(1);
        }
        else if(!strcmp(op,"D"))
        {
            cin>>k;
            k=ph[k];
            heap_swap(k,size);
            down(k);
            up(k);
        }
        else
        {
            cin>>k>>x;
            k=ph[k];
            h[k]=x;
            down(k);
            up(k);
            
        }
           
    }
    return 0;
}

/*
int main()
{
	int n;
	int op, x;
	scanf("%d",&n);
	while (n--)
	{
		scanf("%d",&op);
		if (op == 1)
		{
			scanf("%d",&x);
			siz++;
			h[siz] = x;
			up(siz);
		}
		else if (op == 2)
		{
			printf("%d\n",h[1]);
		}
		else
		{
			h[1] = h[siz];
			siz--;
			down(1);
		}
	}
 
}

24.Hash表

[[哈希]]

(1)作用:把一个复杂的数据结构(值域,数据)映射到[0,n]; n一般是10的五次方或者10的六次方 如将[0, 1 0 9 10^9 109]映射到[0, 1 0 5 10^5 105]

时间复杂度为O(1),一般不会有删除,只会有查找和添加元素。如果想要删除一个数据,也不是真正意义上的删除,而是设定一个布尔数值,布尔值为false时代表删除这个数据。

(2)Hash函数:

int k= (x % N + N)% N;
//将x取余将x映射到[0,N]范围内
// 因为在C++中如果负数取模后仍然是负数,因此需要先取模后加上N再对N取模,其结果一定是一个正数。

(3)冲突:多个数据可能会映射到同一个数值

image-20231024214933898

对于冲突有两种解决办法:开放地址存储法、拉链法

(4)解决办法 / 存储结构

1.拉链法

对于拉链法:N的寻找

N一般是一个质数,而这个质数时第一个大于数据上限的一个质数

void zhishu()
{
    for(int i=100000;;i++)
    {
        bool flag=true;
        for(int j=2;j*j<=i;j++)
        {
            if(i%j==0)
            {
                flag=false;
                break;
            }     
        }
        if(flag == true)
        {
            cout<<i;//这是大于100000的第一个质数,然后把N更新成这个数。
        }
    }
}

//原因就是,取质数,并且这个质数是离2的整数次幂最远,也就是离数据上界最近时,映射发生冲突的概率时最小的
const int N=100003;

void zhishu()
{
    for(int i=100000;;i++)
    {
        bool flag=true;
        for(int j=2;j*j<=i;j++)
        {
            if(i%j==0)
            {
                flag=false;
                break;
            }     
        }
        if(flag == true)
        {
            cout<<i;//这是大于100000的第一个质数,然后把N更新成这个数。
        }
    }
}

//原因就是,取质数,并且这个质数是离2的整数次幂最远,也就是离数据上界最近时,映射发生冲突的概率时最小的
int h[N],e[N],ne[N],idx;

void insert()
{
    int k =(x%N+N)%N;//hash函数
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;
}

bool find(int x)
{
    int k=(x%N+N)%N;
    for(int i=h[k];i!=-1;i=ne[i])//空指针为-1
    {
        if(e[i]==x)
            return true}
    return false;
    
}

int main()
{
    int n;
    cin>>n;
    memset(h,-1,sizeof(h));//把槽清空
    while(n--)
    {
        char op[2];
        int x;
        cin>>op>>x;
        if(op[0]=='I')
            insert(x);
        else
        {
            if(find(x))
                cout<<"YES";
            else
                ocut<<"NO";               
        }
    }
}

2.开放地址寻找法

N的寻找:

一般是数据范围( 1 0 5 10^5 105)的2~3倍,冲突范围比较低

void zhishu()
{
    for(int i=200000;;i++)
    {
        bool flag =true;
        for(int j=2;j*j<=i;j++)
        {
            if(i%j==0)
            {
                flag=false;
                break;
            }
        }
        if(flag==true)
        {
            cout<<i<<endl;
        }
    }
}
const int N=200003,NULL = 0x3f3f3f3f;
//null 是一个不在数据范围(-10^9~10^9)的一个数
/*0x3f3f3f3f表示无穷大,0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即10^9数量级,而一般场合下的数据都是小于10^9的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。*/

void zhishu()
{
    for(int i=200000;;i++)
    {
        bool flag =true;
        for(int j=2;j*j<=i;j++)
        {
            if(i%j==0)
            {
                flag=false;
                break;
            }
        }
        if(flag==true)
        {
            cout<<i<<endl;
        }
    }
}

int find(int x)
{
    int k= (X%N + N)% N;
    while(h[k]!= NULL && h[k]!=x )//这个槽不为空且不为X,就继续
    {
        k++;
        if(k==N)//k到头了,回到0,继续找
            k=0;
    }
    return k;
}

int main()
{
    int n;
    cin>>n;
    memeset(h,0x3f,sizeof(h));
    while(n--)
    {
        char op[2];
        int x;
        cin>>op>>x;
        int k= find(x);
        if(op[0]=='I')
            h[k]=x;
        else
        {
            if(h[k]!=NULL)
                cout<<"YES";
            else
                ocut<<"NO";               
        }
    }
}



字符串哈希方式

①字符串前缀哈希法

str="ABCABCDEYXCACWING";

![image-20231025164401046](https://keriyar-images.oss-cn-qingdao.aliyuncs.com/img/202310251644107.png)

②将一个字符串映射成一个数字

设一个p进制,将字符串映射成一个数(p一般是131 或 13331)(Q=$2^{64}$)

![image-20231025164124698](https://keriyar-images.oss-cn-qingdao.aliyuncs.com/img/202310251641767.png)

如果定义成 unsigned long long 类型的,就不用再mod Q了

unsigned long long的值范围是[0,2^64-1] 所以溢出的话就相当与%2^64

从1开始映射,不能映射成0。

```cpp
if A -> 0;
那么AA 也是-> 0;
相当于将不同的字符串映射成同一个数

③哈希函数

h[R]-h[L]* p R − L + 1 p^{R-L+1} pRL+1

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

求第i位的哈希值:

h[i]=h[i-1]*p+str[i]

例题:

image-20231025170202887

typedef unsigned long long ULL;
int const N=100010,p=131;

int n,m;
char str[N];
ULL h[N],p[N];

ULL get(int l ,int r)
{
    return h[r]-h[l-1]*p[r-1+l];
}

int main()
{
    cin>>n>>m>>str+1;//从str[1]开始读入
    p[0]=1;
    for(int i=1;i<=n;i++)//对每一个字符进行预处理,从1开始
    {
        p[i]=p[i-1]*p;//p的次方
        h[i]=h[i-1]*p+str[i];//前缀哈希值
    }
    while(m--)
    {
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(get(l1,r1)==get(l2,r2))
        {
            cout<<"YES";
        }
        else
        {
            cout<<"NO";
        }
    }
    return 0;
    
}

25.DFS深度优先搜索

[[DFS]]

用栈stack实现的,空间复杂度为O(h),与高度成正比,但是不具有最短路的性质

技巧:回溯,剪枝(剪枝就是在不符合条件时,直接回溯,不再继续)

注意点:递归结束条件的选择+状态标记+递归后的恢复(一定要恢复现场)

DFS一般没有固定的框架,需要灵活变化。

例题:

image-20231031210942665
#include<bits/stdc++.h>
using namespace std;

const int N=10010;
int n;
int path[N];
bool st[N];//默认为false 

void dfs(int u)//u是递归的层数
{
	if(u==n)
	{
		for(int i=0;i<n;i++)cout<<path[i]<<" ";
		cout<<endl;
		return ;//回溯 
		//由于是void,所以返回空就可以 
	}
	for(int i=1;i<=n;i++)
	{
		if(!st[i])
		{
			path[u]=i;
			st[i]=true;//准确来讲应该是st[path] ,就是将已走的路标记一下,不一定是path[u],这个值,但是i是一定的
			dfs(u+1);
			
			//回溯之后要将之前标记的状态st[i]恢复
			//让别的路可以走
			st[i]=false; 
			
		}
	}
}
int main()
{
	cin>>n;
	dfs(0);
	
}

例题:N—皇后问题

image-20231031212349146
#include<bits/stdc++.h>
using namespace std;

const int N=10010;
int n;
char g[N][N];
bool col[N],dg[N],undg[N];
//对于对角线最好开2*N大小。
char path[N];

void dfs(int u)
{
	if(u==n)
	{
		for(int i=0;i<n;i++)puts(g[i]);
		puts("");
		return ;
		
	}
	for(int i=0;i<n;i++)
	{
		if(!col[i] && !dg[u+i] && !undg[n-u+i])
		{
			g[u][i]='Q';//这里的u既是递归的层数,也是地图的行 
			col[i]=dg[u+i]=undg[n-u+i]=true;
			
			dfs(u+1);
			
			//回溯
		 	col[i]=dg[u+i]=undg[n-u+i]=false;
		 	g[u][i]='.';
		}
	}
}


int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			g[i][j]='.';
		}
	}
	dfs(0);
	return 0;
}
image-20231101162808393

由于数组其实是倒置的坐标系,所以正负斜线也是反的。

dg[u+i]; 就是数组坐标系的反斜线,平常坐标系的正斜线,在这条斜线上的所有点的X+Y都等于一个常数b,而一条斜线对应一个b,所以可以直接通过b进行区分

同理undg[n-u+i];也是一样的,不过有时候会出现 i -u等于负数,所以都+n,以保证数组不会越界。都加上n,就相当于没有+n,所以不会对结果有影响。

dg[u+i];//就是数组坐标系的反斜线,平常坐标系的正斜线,在这条斜线上的所有点的X+Y都==一个常数b
undg[n-u+i];

26.BFS广度优先搜索

[[BFS]]

queue实现的,空间复杂度为O( 2 h 2^h 2h),与高度成指数相关,有最短路性质

只有每一条边的权重相同才能用BFS

image-20231101204301729

#include<bits/stdc++.h>
using namespace std;

const int N=100;

int n,m;
int g[N][N];//g存的是地图 
int d[N][N];//d存的是每一个点到起点的距离 
typedef pair<int,int> PII;
queue<PII>q;

int bfs()
{
	q.push({0,0});//起点为(0,0) 
	memset(d,-1,sizeof (d));//将距离初始化为-1
	d[0][0]=0;//起点的距离为0
	
	int dx[4]={-1,0,1,0};
	int dy[4]={0,1,0,-1};
	
	while(q.size())
	{
		auto t= q.front();
		q.pop();
		for(int i=0;i<4;i++)
		{
			int x= t.first + dx[i];
			int y= t.second + dy[i];
			if(x>=0 && x<n && y>=0 && y<m && d[x][y]==-1 && g[x][y]==0)
			//g[x][y]==0是这个点可以走,d[x][y]==0是这个点没有被走过
			{
				d[x][y]=d[t.first][t.second]+1;
				q.push({x,y});
			}
		}
	}
	
	return d[n-1][m-1];
	
	
}

int main()
{
	cin>>n>>m;
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<m;j++)
		{
			cin>>g[i][j];
		}
	}
	cout<<bfs()<<endl;
	return 0; 
}

输出所有走过的路

queue<PII>Prev;//c

int bfs()
{
	q.push({0,0});//起点为(0,0) 
	memset(d,-1,sizeof (d));//将距离初始化为-1
	d[0][0]=0;//起点的距离为0
	
	int dx[4]={-1,0,1,0};
	int dy[4]={0,1,0,-1};
	
	while(q.size())
	{
		auto t= q.front();
		q.pop();
		for(int i=0;i<4;i++)
		{
			int x= t.first + dx[i];
			int y= t.second + dy[i];
			if(x>=0 && x<n && y>=0 && y<m && d[x][y]==-1 && g[x][y]==0)
			//g[x][y]==0是这个点可以走,d[x][y]==-1是这个点没有被走过
			{
				d[x][y]=d[t.first][t.second]+1;
				Prev.push(t);
				q.push({x,y});
			}
		}
	}
	
	
	while(Prev.size())
	{
		auto t =Prev.front();
		int x=t.first,y=t.second;
		cout<<x<<" "<<y<<endl;
		Prev.pop();
	}
	cout<<n-1<<" "<<m-1<<endl;
	
	return d[n-1][m-1];
	
	
}

输出最短路径

queue<PII>q;
PII Prev[N][N];//存储上一个点 

int bfs()
{
	q.push({0,0});//起点为(0,0) 
	memset(d,-1,sizeof (d));//将距离初始化为-1
	d[0][0]=0;//起点的距离为0
	
	int dx[4]={-1,0,1,0};
	int dy[4]={0,1,0,-1};
	
	while(q.size())
	{
		auto t= q.front();
		q.pop();
		for(int i=0;i<4;i++)
		{
			int x= t.first + dx[i];
			int y= t.second + dy[i];
			if(x>=0 && x<n && y>=0 && y<m && d[x][y]==-1 && g[x][y]==0)
			//g[x][y]==0是这个点可以走,d[x][y]==-1是这个点没有被走过
			{
				d[x][y]=d[t.first][t.second]+1;
				Prev[x][y]=t;//存储上一个点 
				q.push({x,y});
			}
		}
	}
	
	
	int x= n-1,y=m-1;
	while(x || y) 
	{
		cout<<x<<" "<<y<<endl;
		auto t = Prev[x][y];//t是终点的最短路径的上一个点
		x=t.first,y=t.second; 
	}
	//最终是倒叙输出 
	
	return d[n-1][m-1];
	
	
}

image-20231105214059159

思路分析:

1.目标状态

八数码目标.JPG

2.移动情况

image-20231105214240061

3.将每一种状态转换为一个结点

将3*3的矩阵转换为字符串

表示方法.JPG

队列可以用 queue<string>;
//直接存转化后的字符串
dist数组用 unordered_map<string, int>;
//将字符串和数字联系在一起,字符串表示状态,数字表示距离
//建立了状态和距离的关系

4.矩阵转换为字符串的方式

矩阵转换.JPG

代码:

#include<bits/stdc++.h>
using namespace std;

queue<string>q;
unordered_map<string,int> d;

int bfs(string start)
{
	//定义目标状态
	string end = "12345678x";
	
	//初始化队列和dist 数组
	q.push(start);
	d[start]=0;
	
	//方向
	int dx[]={1,-1,0,0};
	int dy[]={0,0,1,-1};
	
	while(q.size())
	{
		auto t= q.front();
		q.pop();
		
		//记录当前状态的距离
		int dis=d[t];
		
		if(t==end)
		return dis;
		
		//查询X在字符串中的下标
		int k=t.find('x');
		int x=k/3,y=k%3;
		
		for(int i=0;i<4;i++)
		{
			//转移后的坐标
			int tx=x+dx[i];
			int ty=y+dy[i];
			
			//判断是否越界
			if(tx>=0 && tx<3 && ty>=0 && ty<3)
			{
				//转移X
				swap(t[k],t[tx*3+ty]);
				//如果当前状态是第一次遍历,记录距离,入队
				if(!d.count(t))
				{
					d[t]=dis+1;
					q.push(t);
				}
				//还原状态
				swap(t[k],t[tx*3+ty]); 
			 } 
		}
	}
	//无法转换到目标状态
	return -1; 
}

int main()
{
	string c,start;
	for(int i=0;i<9;i++)
	{
		cin>>c;
		start+=c;
	}
	cout<<bfs(start)<<endl;
	return 0;
}

27.树与图的DFS

树是一种特殊的图,无环连通图

图:有向图、无向图,而无向图是一种特殊的图

有向图的存储:邻接表、邻接矩阵(用的比较少、空间复杂度为O(N^2),主要用于稠密图)

邻接表:存储这个点可以到达那个点

image-20231106212210220 image-20231106212227836

插入新的边,采用头插法

例题:

image-20231106221138189 image-20231106221107977
#include<bits/stdc++.h>
using namespace std;

const int N=10010,M=2*N;
int h[N],e[N],ne[M],idx;
bool st[N];
int n;
int ans=N;//ans就是全局的答案 

void add(int a,int b)//插入一条a->b的边 
{
	e[idx]=b,ne[idx]=h[a],h[a]=idx++;
	
}

//返回以u为根的子树中结点的个数,包括u结点 
int  dfs(int u)
{ 
	st[u]=true;//标记一下,已经被搜过u结点 
	
	int sum=1,res=0;
	//sum存储以u为根的树的结点树,包括u 
	//res是删掉某个节点后,最大的连通子图的结点数 
	
	//访问u的子节点 
	for(int i=h[u];i!=-1;i=ne[i])
	{
		int j=e[i];
		//j是下一个结点 
		if(!st[j])
		{
			int s=dfs(j);//s表示当前子树的大小
            
			res=max(res,s);//记录最大连通子图的节点数 
             //每一次循环res都在更新,最终得到u的子树中的最大连通图的节点数
            
			sum+=s;//以j为根的树的节点数 
		}
		
	}
	
	//n-sum,就是不包括根节点结点 ,的到除了u及其子树以外的连通图的节点数
	res=max(res,n-sum);
    
	ans=min(ans,res);//ans为全局变量,是最终的结果
	return sum;
}

int main()
{
	cin>>n;
	
	memset(h,-1,sizeof(h));
	
	for(int i=0;i<n-1;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
		//建立无向图 
	}
	dfs(1);
	//可以任选一个结点开始,u<=n 
	cout<<ans<<endl;
	return 0;
	
}

28.树与图的BFS

(1)例1:最短距离

image-20231112153633951

#include<bits/stdc++.h>
using namespace std;

const int N=100100;

int n,m;
int h[N],e[N],ne[N],idx;
int d[N];
queue<int>q;

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

int bfs()
{
	q.push(1);
	memset(d,-1,sizeof(d));
	d[1]=0;
	
	while(q.size())
	{
		auto t =q.front();
		q.pop();
		
		for(int i=h[t];i!=-1;i=ne[i])
		{
			int j=e[i];
			if(d[j]==-1)
			{
				d[j]=d[t]+1;
				q.push(j);
			}
		}
	}
	return d[n];
	
}

int main()
{
	cin>>n>>m;
	
	memset(h,-1,sizeof(h));
	for(int i=0;i<m;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
	}
	cout<<bfs()<<endl;
	return 0;
}
 
(2)例二:最短路计数

P1144 最短路计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

image-20231119233609544
#include<bits/stdc++.h>
using namespace std;
//由于是只找最短路径数所以不用求最短路径。
//题目当中是边的wieght一致所以可以用BFS
//在树当中,判断所求结点的深度,即可判断有几条路
const int N=1000100;
int n,m;
int h[N],e[N],ne[N],idx;
int d[N];
bool visited[N];
queue<int>q;
int cnt[N],depth[N];

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

void bfs()
{
	q.push(1);
	visited[1]=true;
	cnt[1]=1;
	depth[1]=0;
	
	while(q.size())
	{
		auto t =q.front();
		q.pop();
		
		for(int i=h[t];i!=-1;i=ne[i])
		{
			int j=e[i];
			if(!visited[j])
			{
				visited[j]=true;
				depth[j]=depth[t]+1;
                  //j的最小深度,最先被访问的jyi'di
				q.push(j);
			}
			if(depth[j]==depth[t]+1)
			{
				cnt[j]=(cnt[t]+cnt[j])%100003;
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		cout<<cnt[i]<<endl;
	}
}

int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=0;i<m;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
	}
	bfs();
 } 

29.拓扑序列

只针对有向图,无向图是没有拓扑序列的

一定是有向无环图,而任意一个的有向无环图一定存在一个拓扑序列,所以有向无环图被称为拓扑图

概念:若一个由图中所有点构成的序列 A 满足,对于图中的每条边 (x,y),x 在 A中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

入度:有几条边进来

出度:有几条边出去

所有入度为0的点都可以作为起点

什么是拓扑排序:

一个有向图,如果图中有入度为 0 的点,就把这个点删掉,同时也删掉这个点所连的边。

一直进行上面出处理,如果所有点都能被删掉,则这个图可以进行拓扑排序。

image-20231112165322410
#include<bits/stdc++.h>
using namespace std;
const int N=100100; 
int n,m;
int h[N],e[N],ne[N],idx;
queue<int>q;
int d[N];//表示点的入度 

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

bool topsort()
{
	for(int i=1;i<=n;i++)//遍历一下顶点的入度 
	{
		if(!d[i])//入度为0,则入队 ,找到起点 
		{
			q.push(i);
		}
	}
	vector<int>ans;//记录答案 
	while(q.size())
	{
		auto t = q.front(); 
		q.pop();
		ans.push_back(t);
		for(int i=h[t];i!=-1;i=ne[i])
		{
			int j=e[i];//j是a的下一个点 
			d[j]--;//j点的入度--
			 
			if(d[j]==0)//找到入度为0的点,就是这个排序的终点 
			{
				q.push(j);
			}
		}
	}
	if(ans.size()==n)
	{
		for(int i=0;i<n;i++)
		{
			cout<<ans[i]<<" ";
		}
	}
	else
	{
		cout<<-1;
	}
	
}
int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);	
	for(int i=0;i<m;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		d[b]++;//b的入度++ 
	}
	topsort();
	return 0;
}

30.最短路径

都是带权重的

1.单源最短路:从一个点到其他所有点的最短距离

​ (1)所有边的权重都是正数:

​ ①朴素Dijkstra算法,O( n 2 n^2 n2),n是点的数量,适合稠密图(边数很多,远大于节点数, m > n 2 m>n^2 m>n2, m是 n 2 n^2 n2级别的),用邻接矩阵来存

​ ②堆优化版的Dijkstra算法,O( m ∗ log ⁡ n m *\log{n} mlogn),m是边的数量,适合稀疏图( m < n l o g n m<nlogn m<nlogn,mn 1 0 5 10^5 105),用邻接表来存

​ (2)存在负权边:

​ ①Bellman-ford,O( n ∗ m n*m nm),点数x边数

​ ②SPFA,O(m),对边数线性相关,最坏为O( n ∗ m n*m nm)

注:SPFA是Bellman-ford的优化,但是当题目要求所经过的边数m<=k时,只能用Bellman-ford

2.多源汇最短路:起点和终点不唯一

​ Floyd算法,O( n 3 n^3 n3),是点的个数,不能有负权回路

image-20231112172419848

31.朴素Dijkstra

【算法】最短路径查找—Dijkstra算法_哔哩哔哩_bilibili

求从1到n的最短距离

重边:一个点到另一个点有多个权重

自环:自己指向自己

image-20231114201927768 image-20231114210813134
#include<bits/stdc++.h>
using namespace std;

const int N=510;
int n,m;
int g[N][N];//图中边的距离
int dist[N];//距离
bool st[N];//判断有没有被访问

int dijkstra()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;//从1开始出发
	
	for(int i=0;i<n;i++)//n次循环,都遍历一遍
	{
		int t= -1;
		for(int j=1;j<=n;j++)//寻找没有被访问过的点中dist最小的点
		{
			if(!st[j] && (t==-1 || dist[t]> dist[j]))
			{
				t=j;
			}
		}
		//t最后求出来的是没有被访问过的距离最近的点
		st[t]=true;
		//t标记为被访问过
		for(int j=1;j<=n;j++)//用t更新其他点到1号点的距离
		{
			dist[j]=min(dist[j],dist[t]+g[t][j]);
            //如果到j的距离比经过t再到j的距离大,那么更新一下
		}
	}
	if(dist[n]==0x3f3f3f3f) 
	return -1;
	else
	return dist[n];
}

int main()
{
	cin>>n>>m;
	memset(g,0x3f,sizeof g);//初始化为正无穷
	
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		g[a][b]=min(g[a][b],c);//防止重边
	}
	cout<< dijkstra();
	return 0;
}
 

32.堆优化版Dijkstra

稀疏图的话,用堆优化一下

#include<bits/stdc++.h>
using namespace std;

const int N=100100;
int n,m;
int h[N],e[N*2],ne[N*2],w[N*2],idx;
int dist[N];
typedef pair<int,int>PII; 
bool st[N];
int a[N];

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

int dijkstra()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	
	priority_queue<PII,vector<PII>,greater<PII> >heap;
	//小根堆,优先级最高的是first最小的,相当于对距离按升序进行优先级排序。
	heap.push({0,1});//将1号点放进来,0是距离,1是结点 
	while(heap.size())
	{
		auto t=heap.top();//每次找到距离最小的点
		heap.pop();
        d.push_back(t.second);
		
		int ver=t.second,distance=t.first;
		 
		if(st[ver])continue;//如果被访问过了,就continue 
		st[ver]=true;
		for(int i=h[ver];i!=-1;i=ne[i])
		{
			int j= e[i];
			if(dist[j]>distance + w[i])
			//如果直接到j的距离比经过t再到j的距离大 ,那么就要更新 
			{
				dist[j]= distance + w[i];
				heap.push({dist[j],j}); 
               
			}
			
		}
	}
	if(dist[n]==0x3f3f3f3f)
	return -1;
	else
	return dist[n];
}

int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	
	cout<<dijkstra();
	return 0;
	
}

33.Bellman-ford

无法处理最短路径上存在负权回路,如果这个负权环不在最短路径上,那么就不影响

image-20231115200127169

例题:

image-20231115210139442
#include<bits/stdc++.h>
using namespace std;

const int N=10010;
int n,m,k;
int dist[N],backup[N];
//backup是上一次的信息

struct Edge
{
	int a,b,w;
}edges[N];//存储边的信息

int bellman_ford()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	for(int i=0;i<k;i++)//k是经过的第k条边
	{
		memcpy(backup,dist,sizeof dist);
        //backup是上一次发生的结果,防止发生串联 
		for(int j=0;j<m;j++)
		{
			int a=edges[j].a,b=edges[j].b,w=edges[j].w;
			dist[b]=min(dist[b],backup[a]+w);
		}	
	}
	
	if(dist[n]>0x3f3f3f3f/2)
        //是为了防止负边权出现将正无穷减小
	{
		return 0x3f3f3f3f;
	}
	else
	{
		return dist[n];
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=0;i<m;i++)
	{
		int a,b,w;
		cin>>a>>b>>w;
		edges[i]={a,b,w};
	}
	int t=bellman_ford();
	if(t==0x3f3f3f3f)
	{
		cout<<"impossible";
	}
	else
	{
		cout<<t;
	}
	
	
}

34.SPFA

SPFA可以处理负权边,但是处理不了负权回路

(1)SPFA求最短路径

spfa算法文字说明:

  • 建立一个队列,初始时队列里只有起始点。

  • 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。

  • 再建立一个数组,标记点是否在队列中。

  • 队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且被点不在队列中,则把该点加入到队尾。

  • 重复执行直到队列为空。

  • 在保存最短路径的数组中,就得到了最短路径。

image-20231115220347976
#include<bits/stdc++.h>
using namespace std;

const int N =100100;
int n,m;
int h[N],e[N],ne[N],w[N],idx;
int dist[N];
bool st[N];

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

void spfa()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0;
	queue<int>q;
	q.push(1);
	st[1]=true;
	while(q.size())
	{
		int t= q.front();
		q.pop();
		
		for(int i=h[t];i!;i=ne[i])
		{
			int j=e[i];
			if(dist[j]>dist[t]+w[i])
			{
				dist[j]=dist[t]+w[i];
				if(!st[j])
				{
					q.push(j);
					st[j]=true;
				}
			}
		}
	}
	
	if(dist[n]== 0x3f3f3f3f)
	{
		cout<<"impossible";
	}
	else
	{
		cout<<dist[n];
	}
}

int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	spfa();
	return 0;
	
}

(2)SPFA判断负权回路/负环

#include<bits/stdc++.h>
using namespace std;

const int N=100100;
int n,m;
int dist[N],cnt[N];
//dist是距离,cnt是边数 
//如果cnt[x]>=n,那么就一定存在环 
int h[N],e[N],ne[N],w[N],idx;
bool st[N];

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

bool spfa()
{
	//memset(dist,0x3f,sizeof dist);
	//不需要初始化了,因为求的是负环,不是最短距离 
	//dist[1]=0;
	queue<int>q;
	for(int i=1;i<=n;i++)
	{
	    st[i]=true;
 		q.push(i);
	}
	
	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];
				cnt[j]=cnt[t]+1;
				
				if(cnt[j]>=n)
				{
					return true;
				}
				if(!st[j])
				{
					q.push(j);
					st[j]=true;
				}
			}
		}
		
	}
	return false;
	
}

int main()
{
 	memset(h,-1,sizeof h); 
	cin>>n>>m;
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
	}
	if(spfa())
	{
		cout<<"Yes";
	}
	else
	{
		cout<<"No";
	}
	return 0;
	
}
 

35.Floyd

用邻接矩阵来存,基于动态规划,不能有负权回路

image-20231116191604236
#include<bits/stdc++.h>
using namespace std;

const int N=500,INF=1e9;
int n,m,q;
int d[N][N];

void floyd()
{
	for(int k=1;k<=n;k++)
	{
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=n;j++)
			{
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
}

int main()
{
	cin>>n>>m>>q;
	
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(i==j)
			{
				d[i][j]=0;
			}
			else
			{
				d[i][j]=INF;
			}
		}
	}
	
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		d[a][b]=min(d[a][b],c);
	}
	floyd();
	
	while(q--)
	{
		int a,b;
		cin>>a>>b;
		if(d[a][b]>INF/2)
		{
			cout<<"impossible"<<endl;
		}
		else
		{
			cout<<d[a][b]<<endl;
		}
		
	}
	return 0;
}

36.最小生成树

最小生成树:就是对于一个有 n 个点的无向连通图的生成树,其包含原图中的所有点,且保持图连通的边权总和最少的边。

简单来说就是对于一个有 n 个点的无向连通图的生成树,其包含原图中的所有点,且保持图连通的边权总和最少的边。

img

最小生成树具有以下两条性质:

  • 切割性质:连接点 x、y 的边权最小的边必定被生成树包含
  • 回路性质:任意回路/环上的边权最大的边必不被生成树包含

1.Prim算法:

​ (1)朴素版的Prim,O( n 2 n^2 n2),适用于稠密图

​ (2)堆优化版的Prim,O( m l o g n mlogn mlogn),适用于稀疏图

2.Kruskal算法:

​ O( m l o g m mlogm mlogm),适用于稀疏图

稠密图一般用朴素Prim,稀疏图一般用Kruskal,堆优化的Prim不常用

边的正负没有影响

37.朴素版Prim

朴素版prim和Dijkstra很相似

思路:

  1. d i s t [ i ] − > + ∞ dist[i]->+\infty dist[i]>+,所有点到集合的距离是正无穷

  2. 找到集合外距离最近的点t

  3. 用t去更新集合外的点到集合的距离

  4. 把t加到集合当中去

注意:
集合外的点到集合的距离是,集合外的点到集合内部的点的距离的最小值

#include<bits/stdc++.h>
using namespace std;

const int N=5100,INF=0x3f3f3f3f;

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

int prim()
{
	memset(dist,0x3f,sizeof dist);
	dist[1]=0; 
	int res=0;//总路径长度
	for(int i=0;i<n;i++)//n次迭代 
	{
		int t=-1;//t初始化
		for(int j=1;j<=n;j++)//从1开始找
		{
			if(!st[j]&&(t==-1 || dist[t]>dist[j]))//如果是第一个点或者距离更近,并且没被访问过,更新t,知道找到一个离集合最近的点
			{
				t=j;
			}
		}
		if(dist[t]==INF)return INF;//孤立点 
		//z
		
		res+=dist[t];
		
		st[t]=true;
        
        //更新其他的点到集合的距离
		for(int j=1;j<=n;j++)
		{
			dist[j]=min(dist[j],g[t][j]);
            /*if(dist[j]>g[t][j] && !st[j])
            {
                dist[j]=g[t][j];
                pre[j]=t;
            }*/
            //得到路径
		}
		 
	}
	return res;
    
    /*for(int i=n;i>1;i--)
    {
     	cout<<i<<" "<<pre[i]<<" "<<endl;   
    }*
}

int main()
{
	cin>>n>>m;
	memset(g,0x3f,sizeof g);
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		g[a][b]=g[b][a]=min(g[a][b],c);
	}
	int t =prim();
	if(t==INF)
	{
		cout<<"i";
	}
	else
	{
		cout<<t;
	}
	return 0;
}
 

38.Kruskal

思路:

将所有边按权重从小到大排序,依赖sort函数,所以其时间复杂度的上限就是sort的复杂度 m l o g m mlogm mlogm

枚举每一条边a,b,边权c,如果ab不连通,就将这条边加入到集合中

用到了并查集的知识。

image-20231121213454618
#include<bits/stdc++.h>
using namespace std;

const int N=200100;
int n,m;
int p[N];//每个点的父节点
int res;//最小生成树中的边权之和
int cnt;//加了多少条边 
struct Edge
{
	int a,b,w;
	bool operator < (const Edge &W)const 
	{
		return w<W.w;
	}//重载比较运算符
}edges[N];

int find(int x)
{
	if(p[x]!=x)
	{
		p[x]=find(p[x]);
	}
	return p[x];
}
int main()
{
	cin>>n>>m;
	for(int i=0;i<m;i++)
	{
		int a,b,w;
		cin>>a>>b>>w;
		edges[i]={a,b,w};
	}
	sort(edges,edges+m);
	//按照边权进行排序
	for(int i=1;i<=n;i++)
	{
		p[i]=i;//每个点的根节点是自己
	}
	
	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);//找a和b的父节点
		if(a!=b)//a和b不在一个集合中
		{
			p[a]=b;//b成为a的父节点,合并a和b
			res+=w;//权重和更新
			cnt++;//边数++
            //cout<<edges[i].a<<" "<<edges[i].b<<" "<<edges[i].w<<endl;
            //这是在输出每一次选择的边
		}
	}
	if(cnt < n-1)//如果遍历的边数不足n-1,说明有的点无法来连通
	{
		cout<<"impossible";
	 } 
	 else
	 {
	 	cout<<res;
	 }
	return 0;
}
 

39.二分图

概念:就是顶点集 V 可分割为两个互不相交的子集,且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。

简单的说就是:它可以被划分为两个部分,每个部分内的点互不相连。

当图中的顶点分为两个集合,使得第一个集合中的所有顶点都与第二个集合中的所有顶点相连时,此时是一特殊的二分图,称为完全二分图。

1.染色法:O(m+n)

2.匈牙利算法:O(mn),一般来说实际运行时间远小于O(mn)

40.染色法

判断是否为二分图

image-20231123200926965
#include<bits/stdc++.h>
using namespace std;

const int N=100010,M=2*N;
int h[N],e[M],ne[M],idx;
int n,m;
int color[N];

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

bool 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])//如果未被染色 
		{
			if(!dfs(j,3-c))//接下来的未被染色成功 
			{
				return false;
			}
		}
		else if(color[j]==c)//颜色相同 
		{
			return false;
		}
	}
	return true;//染色成功
}
int main()
{
	cin>>n>>m;
	memset(h,-1,sizeof h);
	
	while(m--)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);//创建无向图
	}
	bool flag=true;
	for(int i=1;i<=n;i++)
	{
		if(!color[i])
		{
			if(!dfs(i,1))
			{
				flag=false;
				break;
			}
			
		}
	}
	
	if(flag)
	{
		cout<<"Yes";
	}
	else
	{
		cout<<"No";
	}
	return 0;
	
	
}

41.匈牙利算法

算法学习笔记(5):匈牙利算法 - 知乎 (zhihu.com)

匈牙利算法主要用于解决一些与二分图匹配有关的问题:求二分图的最大匹配数最小点覆盖数

(1)最大匹配数:

二分图的最大匹配数。

二分图的匹配:给定一个二分图 G,在 G的一个子图 M 中,M 的边集 {E}中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。

二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。

image-20231123203030923

例题:

image-20231123203316127
 #include<bits/stdc++.h>
using namespace std;

const int N=100100;
int n1,n2,m;
int h[N],e[N],ne[N],idx;
bool st[N];
int match[N];

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

bool find(int x)
{
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int j=e[i];
		if(!st[j])//这个女生没有被访问 
		{
			st[j]=true;
			if(match[j]==0 || find(match[j]))
			//如果此时这个女生没有被匹配过
			//或者是这个女生被别的男生匹配过了,但是之前这个女生之前被匹配过的这个男生可以找到下架可以找到下家 
			{
				match[j]=x;//将这个女生的匹配更新成这个男生
				return true; 
			}
		}
	}
	return false;
}
int main()
{
	cin>>n1>>n2>>m;
	memset(h,-1,sizeof h);
	
	for(int i=0;i<m;i++)
	{
		int a,b;
		cin>>a>>b;
		add(a,b);
		
	}
	int res=0;//能成功匹配的数量 
	for(int i=1;i<=n1;i++)
	{
		memset(st,false ,sizeof st);
		//将所有的女生初始化为false,因为不知道这个男生有多少个女生。 
		if(find(i))//看看能否匹配成功,这个i代表的是男生 
		{
			res++;
		}
	}
	cout<<res;
}
 

42.质数

[[数论]]

(1)朴素筛
int IsPrime(int n)
{
    int i;
    if(n<2)
        return 0;
    else
    {//函数其他部分不变
        for(i=2;i*i<=n;i++)//这里用i替代了开根号的过程
        {//这里应该取等号,需要取到i=sqrt(n)这个值
            if(n%i==0)
                return 0;
        }
        return 1;
    }
}

(2)埃氏筛

埃氏筛法利用了一个素数的倍数一定不是素数、任何一个合数可以表示成一个素数和另一个数乘积的性质。

对于一定的范围,先假定它们都是质数,然后从2(显然是素数)开始,先判断如果是素数,把它在范围内的倍数乘积都筛去(是合数),以此类推循环至 sqrt(n) 即可。

时间复杂度为 O ( n log ⁡ e ( log ⁡ e n ) ) O(n \log_{e}{(\log_{e}{n})}) O(nloge(logen)) O(n log log n)

这里引入一个数学定理–唯一分解定理,也是算术基本定理

算术基本定理可表述为:任何一个大于 1 的自然数N, 如果 N 不为质数,那么 N 可以唯一分解成有限个质数的乘积—-唯一分解定义百度百科

①存放素数

bool is_prime[1000];//布尔数组来标记是否为素数
int prime[1000];    //存放素数
//也可以用vector<int>prime;
int q = 0;

void isprime_B(int b) //要筛选素数的区间右端点
{
    memset(is_prime,true,sizeof(is_prime));//先假设都为素数
    for(int i = 2;i <= sqrt(b);i++)
    {
        if(is_prime[i])
        {
            prime[q++] = i;
            //prime.push_back(i);
            for(int j = i*2;j <= b;j += i)//素数的倍数一定不是素数
            {
                is_prime[j] = false;
            }
        }
    }

}

②不存放素数

const int MAX = 1000005;
bool prime[MAX];
void init()//素数筛
{
	memset(prime,true, prime);
	for(int i=2; i*i<MAX; i++) 
    {
		if(prime[i]) 
        {//是素数 
			for(int j=i*2; j<MAX; j+=i)//从2倍开始,n倍 
				prime[j]=false;//各个倍数 
		}
	}
} 
(3)欧拉筛

欧拉筛是用当前遍历到的数字i,去乘以已经在素数表中的素数。

首先,这样就保证了是以素数进行倍增,相较于使用任何数字进行倍增的情况,是已经优化过了。比如说此时i=6,前面有素数2,3,5。在欧拉筛中就是用2,3,5去乘以6,进行倍增筛除。因为 i 是从小到大进行循环,会乘以前面的每一个素数,这就保证了每个素数的倍数都不会被错过。并且每个素数的倍增过程都是从平方开始,就和前面埃氏筛优化方法一种一样,可以有效避免重复。

举例说明:现在 i 循环到6,前面有素数2,3,5,这三个素数,都是从22,33,55开始倍增的,不会出现23,3*2的情况同时出现。

//欧拉筛法->适用于一定范围的元素的筛选,时间复杂度O(n)
bool is_prime_Euler[1000];
int prime2[1000] = {0};

void isprime_C(int b)
{
    int k = 0,j = 0;

    memset(is_prime_Euler,true,sizeof(is_prime_Euler));
    for(int i = 2;i <= b;i++)
    {
        if(is_prime_Euler[i])
        {
            prime2[j++] = i;
        }
        //接下来进行筛的操作
        while(1)
        {
            if(i*prime2[k] > b)
            {
                break;
            }
            is_prime_Euler[i*prime2[k]] = false;//最小质因数的倍数一定不是素数
            if(i % prime2[k] == 0)//确保是最小质因数
            {
                break;
            }
            k++;
        }
        k = 0;
    }
}

(4)素因子
#include<bits/stdc++.h>
using namespace std;
const int MAX=100005,N=100005;
vector<int>v;
int temp;
bool prime[N];

void is_prime()
{
    memset(prime,true,sizeof prime);
    for(int i=2;i*i<MAX;i++)
    {
        if(prime[i])
        {
            for(int j=i*2;j<MAX;j+=i)
            {
                prime[j]=false;
            }
        }
    }
}
int main()
{
    cin>>temp;
    for(int i=2;i<temp;i++)
    {
        if(temp%j==0 && prime[j])
        {
            v.push_back(j);//v存的就是素因子
        }
    }
}

43.约数

44.欧拉函数

45.快速幂

快速幂算法 超详细教程-CSDN博客

C++ math库中的pow函数:

double pow( double base, double exp );
//返回值:x不能为负数且y为小数,或者x为0且y小于等于0,返回幂指数的结果。返回类型:double型,int,float会给与警告!

普通求幂指数:

/**
 * 普通的求幂函数
 * @param base 底数
 * @param power  指数
 * @return  求幂结果的最后3位数表示的整数
 */
long long normalPower(long long base,long long power){
    long long result=1;
    for(int i=1;i<=power;i++){
        result=result*base;
    }
    return result%1000;
}
 

快速幂:

img

取模的运算法则:

  1. (a + b) % p = (a % p + b % p) % p (1)
  2. (a - b) % p = (a % p - b % p ) % p (2)
  3. (a * b) % p = (a % p * b % p) % p (3)

借助这个法则,只需要在循环乘积的每一步都提前进行“取模”运算,而不是等到最后直接对结果“取模”,也能达到同样的效果。

快速幂的思想就是拆分指数:

快速幂算法的核心思想就是每一步都把指数分成两半,而相应的底数做平方运算。这样不仅能把非常大的指数给不断变小,所需要执行的循环次数也变小,而最后表示的结果却一直不会变。

3 10 = 9 5 = 9 4 × 9 1 = 8 1 2 × 9 1 = 656 1 1 × 9 1 = 656 1 0 × 656 1 1 × 9 1 = 59049 3^{10}=9^5=9^4 \times9^1 =81^2 \times 9^1=6561^1 \times 9^1 =6561^0 \times 6561^1 \times 9^1=59049 310=95=94×91=812×91=65611×91=65610×65611×91=59049

long long fastPower(long long base, long long power) {
    long long result = 1;
    while (power > 0) {
        if (power % 2 == 0) {
            //如果指数为偶数
            power = power / 2;//把指数缩小为一半
            base = base * base % 1000;//底数变大成原来的平方
        } else {
            //如果指数为奇数
            power = power - 1;//把指数减去1,使其变成一个偶数
            result = result * base % 1000;//此时记得要把指数为奇数时分离出来的底数的一次方收集好
            power = power / 2;//此时指数为偶数,可以继续执行操作
            base = base * base % 1000;
        }
    }
    return result;
}

压榨性能再优化:


long long fastPower(long long base, long long power) {
    long long result = 1;
    while (power > 0) {
        if (power & 1) {//此处等价于if(power%2==1)
            result = result * base % 1000;
        }
        power >>= 1;//此处等价于power=power/2
        base = (base * base) % 1000;
    }
    return result;
}

矩阵的快速幂

#include<bits/stdc++.h>
using namespace std;

const int Mod = 1e9+7;
struct Matrix
{
    long long c[101][101];

} A, I;
long long n, k;
Matrix operator * (const Matrix &x,const Matrix &y)
{//重载运算符
    Matrix a;
    memset(a.c, 0, sizeof a.c);
    for (int i = 1; i <= n;i++)
    {
        for (int j = 1; j <= n;j++)
        {
            for (int k = 1; k <= n;k++)
            {
                a.c[i][j] += x.c[i][k] * y.c[k][j] % Mod;
                a.c[i][j] %= Mod;
            }
        }
    }
    return a;
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i <= n;i++)
    {
        for (int j = 1; j <= n;j++)
        {
            cin >> A.c[i][j];
        }
    }

    for (int i = 1; i <= n;i++)
    {
        I.c[i][i] = 1;
    }

    while(k>0)
    {
        if(k & 1)
        {
            I = I * A;
        }
        A = A * A;
        k >>= 1;
    }

    for (int i = 1; i <= n;i++)
    {
        for (int j = 1; j <= n;j++)
        {
            cout << I.c[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

46.扩展欧几里得算法

47.中国剩余定理

48.高斯消元

49.求组合数

50.容斥原理

51.博弈论

简单博弈论_d: 简单博弈-CSDN博客

(1)巴什博弈

巴什博奕-CSDN博客

一、最基本的巴什博弈

针对一堆物品的拿取

只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。A先取,B后取。

①最后先取光者获胜

如果对于 n=m+1 个物品,那么由于一次最少能取1个,最多能取m个,所以无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,后者取胜。
即如果n = m + 1; 我们假设第一个人拿走了k个, 还剩下 m + 1 - k。 因为1<=(m + 1 - k)<= m, 所以, 剩下的部分一定可以被第二个人一次性取走。

将这一规律进行推广:
如果 n = (m+1) r + s,(r ∈ N,s <= m),意即 n%(m+1) != 0,则 A 第一次先拿走 s 个物品,剩下的即为(m+1)的倍数r(m+1),那么还需要 r 轮能够全部取完,每一轮中,若 B 拿走 k 个,那么 A 可以相应策略地取走(m+1-k)个,这样最后一轮 A 可以保证全部取完,A 获胜。否则,若n%(m+1)== 0,则B获胜。

if(n%(m+1)!=0)
    printf("A\n");
else
    printf("B\n");

②先取光者输

谁都不想输,由于每一轮至少取一个,那么每个人都想最好给对方留下只剩一个(如果不是一个,那么对方会再留给自己一些的),那么对方一下子取完,自己就能获胜。
如果n-1=m+1,如果A取k个,那么B肯定取m+1-k个,从而给A留下一个,B获胜。
将这一规律进行推广:
如果n-1=(m+1)*r,即(n-1)%(m+1)==0,那么会进行r轮,每一轮中A先取k个,B会相应取m+1-k个,最后B取完只剩下1个,A把全部取完,B获得胜利。

if((n-1)%(m+1)!=0)
    printf("A\n");
else
    printf("B\n");

二、巴什博弈演变1:

Alice和Bob在玩这样的一个游戏, 给定 k 个数 a1,a2,a3,……ak。 一开始, 有 x 枚硬币, Alice 和 Bob 轮流取硬币, 每次所取硬币的枚数硬顶要在 a1,a2,a3,……ak之中。 Alice先取, 取走最后一枚硬币的一方获胜。 当双方都采取最优策略的时候, 谁会获胜? 题目假定a1,a2,a3,……ak 之中一定有 1。

分析轮到自己时还有m枚硬币的胜负情况:

  1. 取光所有硬币的获胜,也就是说轮到自己时没有硬币了,那就输了。因此m=0时是必败态
  2. 如果对于某个i,存在(1<=i<=k),(m-a[i])是必败态,那么j就是必胜态了(意即,当前有m枚硬币,自己取了a[i]枚之后剩下的m-a[i]枚导致对手必败,那么自己就是必胜的咯)
  3. 如果对于任意的i,m-a[i]都是必胜态的话,那么m就是必败态咯(意即,当前有m枚硬币,无论自己怎么取,剩下的硬币都会使对手胜利,那么自己就必败无疑啦)

按照规则,利用动态规划,令m从0开始从小到大计算必胜态,大数的状态由小数获得哦。只需最后看x的状态即可。
用bool win[]数组记录每一个数字记录的状态。 只要m-a[i]中的任意一个是必败态,那么m就可为必胜态,即win[m]=(!win[m-a[1]])||(!win[m-a[2]])||…||(!win[m-a[k]]) //(数组a[]需要从小到大排列哦)(一真则真,全假为假)

简单来说:只要目前剩余的硬币一个人可以直接拿完,那么他一定赢,根据这个写出 dp 方程 win[i] = ( !win[m - a[1] || !win[m - a[2] ] || … !win[m - a[k]] )(win[i] 表示在拿某个情况的时候为 0 ,则表示成功)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int x,k;
bool win[100000];//表示轮到某一个人时剩下i个硬币,胜负情况 ,为真则该人必胜,为假则必败 
int a[100000];
void Bash()
{
    win[0]=false;//先取玩的赢了
    //win[0]=true;表示先取玩的输了
    for(int i=1;i<=x;i++)
    {
        win[i]=false;
        for(int j=0;j<k;j++)
            win[i]|=(a[j]<=i&&!win[i-a[j]]); 
    }//a[j]<=i表示还可以取a[j]个硬币,win[i-a[j]]为假表示一方取完(i-a[j])个硬币之后,对手方是必败的,那么win[i]为真则表示此方必胜 
}
int main()
{
    while(~scanf("%d%d",&x,&k))
    {
        for(int i=0;i<k;i++)
            scanf("%d",&a[i]);
        sort(a,a+k);
        memset(win,false,sizeof(win));
        Bash();
        if(win[x])
            printf("A\n");
        else
            printf("B\n");
    }
    return 0;
}

三、巴什博弈演变2:

n枚硬币排成一个圈。Alice和Bob轮流从中取一枚或者两枚硬币, 不过, 取两枚时这两枚必须是连续的。硬币取走之后留下空位,相隔空位的硬币被认为是不连续。**A开始先取, 取走最后一枚硬币的一方获胜。**双方都采取最优策略的时候谁会取胜?

简单来说,就是后者根据前者所取硬币的个数做出相应的对策,从而使得剩下的硬币呈对称状态,(每一次后拿的人,一定让这个圈变成两个组以上并且对称)也就是说两段留出相同数量。所以当总个数n<=2时,先手赢,n >= 2 时,后手赢。

if(n<=2)
    printf("A\n");
else
    printf("B\n");
(2)威佐夫博弈

固定类型,黄金分割

两堆各若干个物品,两个人轮流从任意一堆中取出至少一个 或者 同时从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光者胜利。

假设现在的局势是(0, 0),那么就是后手胜。
假设现在的局势是(1,2),那么先手只有四种取法,均是后手胜(略)
假设现在的局势是(3,5)第一个人无论取多少,第二个人的取完后,一定同局势 2 相同,所以后手获胜。
以此类推,得出规律:
第一种(0,0)

第二种(1,2)

第三种(3,5)

第四种 (4 ,7)

第五种(6,10)

第六种 (8,13)

第七种 (9 , 15)

第八种 (11 ,18)

第n种 ( a[k],b[k] )

我们把这些局势称为“奇异局势”

继续分析我们会发现,每种奇异局势的第一个值 (这里假设第一堆数目小于第二堆的数目)总是等于 当前局势的差值 乘 1.618

a[k] = (int) ( b[k] - a[k] )*1.618 (强制类型转化表示 向下取整)
要求更高精度为: 1.618 = ( s q r t ( 5.0 ) + 1 ) / 2 1.618 = (sqrt(5.0) + 1) / 2 1.618=(sqrt(5.0)+1)/2

if((a[k] - b[k])*(sqrt(5.0) + 1)/2 == b[k]) 
//就满足后手胜的条件
//注意是5.0,不是5
(3)尼姆博弈

针对多堆物品

若干堆石子,每堆石子的数量是有限的,二个人依次从这些石子堆中拿取任意的石子,至少一个(不能不取),最后一个拿光石子的人胜利。

基本推理:
1、首先以一堆为例: 假设现在只有一堆石子,你的最佳选择拿走所有石子,那么你就赢了。
2、如果是两堆:假设现在有两堆石子且数量不相同,那么你的最佳选择是取走多的那堆石子中多出来的那几个,使得两堆石子数量相同,这样,不管另一个怎么取,你都可以在另一堆中和他取相同的个数,这样的局面你就是必胜。
3、如果是三堆 ,我们用(a,b,c)表示某种局势,首 先(0,0,0)显然是奇异局势(必败局势)。第二种奇异局势是 (0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情型。

基础的模板

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

/*
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0
*/

int main(){
    int n;
    scanf("%d", &n);
    int res = 0;
    for(int i = 0; i < n; i++) {
        int x;
        scanf("%d", &x);
        res ^= x;
    }
    if(res == 0) puts("No");
    else puts("Yes");
}

一个Nim问题可以转换为基础模板

52.动态规划

代码随想录 (programmercarl.com)

思考DP的步骤:

  1. dp数组以及下标的含义: d p [ i ] [ j ] 、 d p [ j ] dp[i][j]、dp[j] dp[i][j]dp[j]
  2. 递推公式
  3. dp数组如何初始化
  4. 遍历顺序
  5. 打印dp数组

从两个角度考虑:

一、状态表示 f ( i , j ) f(i,j) f(i,j)

  1. 集合 i , j {i,j} i,j
    1. 所有的选法
    2. 条件:
      1. 只从前 i 个物品选
      2. 选出来的物品的总体积 <= j
  2. 属性 f ( i , j ) f(i,j) f(i,j):最大价值、最小价值、数量

二、状态计算——集合划分:包含第 i 的集合,不包含 i 的集合

  1. 不包含 i 的集合的最大值为 f ( i − 1 , j ) f(i-1,j) f(i1,j)
  2. 包含 i 的集合的最大值为 f ( i − 1 , j − v i ) + w i f(i-1,j-v_i)+w_i f(i1,jvi)+wi
  3. 最后的最大值是两者取最大值: f ( i , j ) = m a x { f ( i − 1 ) , f ( i − 1 , v i − 1 ) + w i } f(i,j)=max\{f(i-1),f(i-1,v_i-1)+w_i\} f(i,j)=max{f(i1),f(i1,vi1)+wi},这就是最终的状态转移方程

image-20231125163341875

三、DP问题的时间复杂度计算

时间复杂度=状态的数量*转移的计算量

53.背包问题

image-20231124151834813
(1)0-1背包

动态规划是不断决策求最优解的过程,「0-1 背包」即是不断对第 i 个物品的做出决策,「0-1」正好代表不选与选两种决定。

N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。


二维版普通版:

image-20231125185559069

#include<bits/stdc++.h>
using namespace std;

const int N = 100100;
int n, m;
int v[N], w[N];
int f[N][N];// f[i][j], j体积下前i个物品的最大价值 


int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];//输入体积和价值
    }
    for (int i = 1; i <= n;i++)//遍历的是物品
    {
        for (int j = 0; j <= m;j++)//遍历的是背包
        {
            f[i][j] = f[i - 1][j];//
            if(j>=v[i])
            {
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
            }
        }
    }

    cout << f[n][m] << endl;
    return 0;
}

一维的滚动数组版:

在使用二维数组的时候,递推公式: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i])

其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是: d p [ i ] [ j ] = m a x ( d p [ i ] [ j ] , d p [ i ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) ; dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]); dp[i][j]=max(dp[i][j],dp[i][jweight[i]]+value[i]);

与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层

#include <bits/stdc++.h>
using namespace std;

const int N = 100100;
int n, m;
int v[N], w[N];
int f[N];//容量为j的背包所背最大值为f[j ]

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j = m; j >= v[i]; j--)//对背包的容量倒叙遍历,保证每个物品只能被加入一次
        {
                f[j] = max(f[j], f[j - v[i]] + w[i]);
            
        }
    }

    cout << f[m] << endl;
    return 0;
}

为什么 j 要倒叙遍历:

倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

为什么倒序遍历,就可以保证物品只放入一次呢?

倒序就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。

那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?

因为对于二维dp, d p [ i ] [ j ] dp[i][j] dp[i][j]都是通过上一层即 d p [ i − 1 ] [ j ] dp[ i - 1][j] dp[i1][j]计算而来,本层的 d p [ i ] [ j ] dp[i][j] dp[i][j]并不会被覆盖!

(2)完全背包

有 N种物品和一个容量是 V的背包,每种物品都有无限件可用。

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

朴素做法:

image-20231130170937386

但是时间复杂度比较高, O ( N ∗ V 2 ) O(N*V^2) O(NV2),是立方级别

#include<bits/stdc++.h>
using namespace std;
const int N = 100100;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;//第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
    for (int i = 1; i <= n;i++)
    {
        cin >> v[i] >> w[i];//第 i 种物品的体积和价值。
    }
    for (int i = 1; i <= n;i++)
    {
        for (int j = 0; j <= m;j++)
        {
            for (int k = 0; k * v[i] <= j;k++)
            {
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    }

    cout << f[n][m];
    return 0;
}

优化版:

image-20231130172900289

对状态转移方程进行优化,发现可以优化掉k,只保留i 和 j,使得时间复杂度降为 O ( N ∗ V ) O(N*V) O(NV),是平方级别

#include<bits/stdc++.h>
using namespace std;
const int N = 100100;

int n, m;
int v[N], w[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n;i++)
    {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n;i++)
    {
        for (int j = 0; j <= m;j++)
        {
            f[i][j] = f[i - 1][j];
            if(j>= v[i])
            {
                f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
            }
        }
    }

    cout << f[n][m];
    return 0;
}

image-20231130173642364

两者的区别在于0-1背包都是从上一次的状态转移来的,而完全背包是从这一层转移来的。

终极优化:优化成一维

#include<bits/stdc++.h>
using namespace std;
const int N = 100100;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n;i++)
    {
        cin >> v[i] >> w[i];
    }
    for (int i = 1; i <= n;i++)
    {
        for (int j = v[i]; j <= m;j++)//c
        {
                f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }

    cout << f[m];
    return 0;
}
(3)多重背包

有 N 种物品和一个容量是 V的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

image-20231201152422786

朴素版:

时间复杂度为: O ( N ∗ V ∗ S ) O(N*V*S) O(NVS)

#include<bits/stdc++.h>
using namespace std;
const int N = 100100;

int n, m;
int v[N], w[N],s[N];
int f[N][N];

int main()
{
    cin >> n >> m;//第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
    for (int i = 1; i <= n;i++)
    {
        cin >> v[i] >> w[i]>> s[i];
    }
    for (int i = 1; i <= n;i++)
    {
        for (int j = 0; j <= m;j++)
        {
            for (int k = 0; k <= s[i] && k * v[i] <= j;k++)
            {
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
            }
        }
    }

    cout << f[n][m];
    return 0;
}

二进制优化版:

时间复杂度为: O ( N ∗ V ∗ l o g S ) O(N*V*logS) O(NVlogS)

二进制优化:

我们想如果我想要拿512件物品,按照转化成01背包的方法做,我们是需要从拿1件枚举到拿512件的,而二进制优化也就带了那么点倍增思想,我们把拿多少件物品分为拿1 2 4 8 16 … 256 512 … 2^n 件,我们枚举的时候就枚举9次 就到了512件了。还有无论是多少件,我们总能用一些数的组合来表示,比如7 就可以用 1 + 2 + 4来表示,只需要枚举3次。这就是我们二进制优化的思想。

推广到广泛的数 S
S 可以表示成 1 , 2 , 4 , 8 , . . . 2 k + c c < 2 k + 1 , 2 k 是最大的一个 < = s 的数 经过二进制优化, S 就可以用 2 k + c 来表示 对于从 1 到 S 的遍历就可以从 O ( S ) ,降为 O ( l o g S ) S可以表示成 1,2,4,8,...2^k+c\\ c<2^{k+1},\quad 2^k是最大的一个<=s的数\\经过二进制优化,S就可以用2^k+c来表示\\对于从1到S的遍历就可以从O(S),降为O(logS) S可以表示成1,2,4,8,...2k+cc<2k+1,2k是最大的一个<=s的数经过二进制优化,S就可以用2k+c来表示对于从1S的遍历就可以从O(S),降为O(logS)
此时时间复杂度就降为 O ( N ∗ V ∗ l o g S ) O(N*V*logS) O(NVlogS)

#include<bits/stdc++.h>

using namespace std;
const int N = 240000;
int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;//物品种数和背包容积
    int cnt = 0;
    for (int i = 1; i <= n;i++)//进行二进制优化
    {
        int a, b, s;//第i种物品的体积、价值和数量。
        cin >> a >> b >> s;
        int k = 1;
        while(k<=s)
        {
            cnt++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if(s>=0)
        {
            cnt++;
            v[cnt] = a * s;
            w[cnt] = b * s;

        }
    }
    n = cnt;
    for (int i = 1; i <= n;i++)
    {
        for (int j = m; j >= v[i];j--)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];
    return 0;
}
(4)分组背包
image-20231201222521791

集合:只从前i 组物品中选,且总体积不大于j的所有选法

image-20231201222457948
#include<bits/stdc++.h>
using namespace std;

const int N = 1000;

int n,m;
int v[N][N], s[N], w[N][N];
int f[N];

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

    for (int i = 1; i <= n;i++)
    {
        for (int j = m; j >= 0;j--)
        {
            for (int k = 0; k < s[i];k++)
            {
                if(v[i][k]<=j)
                {
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
                }
            }
        }
    }
    cout << f[m];
    return 0;
}

54.线性DP

[[线性DP]]

分析状态表示和状态计算

注意:

区间的边界问题,考略越界,状态转移方程的边界,max/min函数的取值问题,如果取值有负数,初始化为负无穷

(1)最长上升子序列

895. 最长上升子序列 - AcWing题库

时间复杂度为 O ( N 2 ) O(N^2) O(N2)

#include<bits/stdc++.h>
using namespace std;

const int N = 100010;
int n;
int a[N];
int f[N];
//f[i]表示所有以i结尾的上升子序列中,最大的一个。
//因为同一个数可以在序列中不同的位置存在,所以要找一个最大的
 int res;
int main()
{
    cin >> n;
    for (int i = 1; i <= n;i++)
    {
        cin >> a[i];
    }
    for (int i = 1; i <= n;i++)
    {
        f[i] = 1;//a[i]这个数自己就是1
        for (int j = 1; j < i;j++)
        {
            if(a[j]<a[i])
            {
                f[i] = max(f[i], f[j] + 1);
                //找a[i]这个数的最大的上升子序列
            }
        }
    }
    for (int i = 1; i <= n;i++)
    {
        res = max(res, f[i]);
    }
    cout << res;
    return 0;
}

image-20240403215148612

55.区间DP

56.计数DP

57.数位DP

58.状态压缩DP

59.树形DP

60.记忆化搜索

61.贪心

62.区间问题

ST

63.Huffman树

64.排序不等式

65.绝对值不等式

66.推公式

[[递推]]

[[递归]]

67.最小公倍数

#include <bits/stdc++.h>
using namespace std;

int GCD(int a, int b)//求最大公约数
{
    return a % b == 0 ? b : GCD(b, a % b);
}
//如果a是b的倍数,那么返回b,否则返回GCD(b,a%b),也就是除数和余数

int LCM(int a, int b)//求最小公倍数
{
    return a * b / GCD(a, b);
}


68.求逆序对数

(1)归并排序: O ( N l o g n ) O(N log{n}) O(Nlogn)

/*1.确定分界点mid、2.递归排序、3.归并-合二为一、*/
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int n;
int a[N], tmp[N];
typedef long long LL;

LL merge_sort(int q[], int l, int r)
{
    if (l >= r)
        return 0;
    int mid = r + l >> 1;
    LL res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
    {
        if (q[i] <= q[j])
        {
            tmp[k++] = q[i++];
        }
        else
        {
            tmp[k++] = q[j++];
            res = (res + mid - i + 1);
        }
    }
    while (i <= mid)
        tmp[k++] = q[i++];
    while (j <= r)
        tmp[k++] = q[j++];
    for (int i = l, j = 0; i <= r; i++, j++)
        q[i] = tmp[j];
    return res;
}
int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
     
    cout << merge_sort(a, 0, n - 1) << endl;
    return 0;
}

(2)树状数组: O ( N l o g n ) O(N log{n}) O(Nlogn)

68.归并排序

[[归并排序]]

/*1.确定分界点mid、2.递归排序、3.归并-合二为一、*/
#include <bits/stdc++.h>
using namespace std;

const int N = 100010;
int n;
int a[N], tmp[N];

void merge_sort(int q[], int l, int r)
{
    // l,r是要进行排序的边界,闭区间
    if (l >= r)
        return;
    int mid = l + r >> 1;
    merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
    int k = 0;              // k表示已经合并了几个数,也就是tmp里面的个数
    int i = l, j = mid + 1; // i,j是两个指针,分别再两个区间的起点
    while (i <= mid && j <= r)
    {
        // 谁小把谁先加入tmp
        if (q[i] <= q[j])
            tmp[k++] = q[i++];
        else
            tmp[k++] = q[j++];
    }
    // 为了防止有的区间没有访问完全,进行额外的判断
    while (i <= mid)
        tmp[k++] = q[i++];
    while (j <= r)
        tmp[k++] = q[j++];

    for (int i = l, j = 0; i <= r; i++, j++)
    {
        q[i] = tmp[j];
    }
}
int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    merge_sort(a, 0, n - 1);
    for (int i = 0; i < n; i++)
    {
        cout << a[i]<<" ";
    }
}

image-20240328193420667

69.暴力枚举

[[暴力枚举、模拟]]

70.日期问题

[[日期问题]]

71.图论

[[图论]]

  • 25
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值