算法竞赛入门基础篇

算法基础整理

上传的目的一方面是备份笔记,另一方面也分享给大家。

文章目录

  • 算法基础整理
    • 一、常用函数及算法
      • (1)进制
        • 1)十进制转R进制(2<= R<=36)
        • 2)R进制转十进制
      • (2)判断素数
        • 方法1
        • 方法2
      • (3)字符串
      • (4)统计字母出现次数(C++)
      • (5)常见ASCII码
      • (6)字符串转整型
      • (7)整型转字符串
      • (8)ToLower实现
      • (9)String中find函数的用法
      • (10)substr的用法
      • (11)字符串分割方法(以任意长度空格分割,注意只能以空格分割)
      • (12)x >> i & 1含义
      • (13)运行时间复杂度分析
      • (14)读取二维字符数组的方式(方便的一种)
    • 二、基础算法
      • (1)排序
        • 1)冒泡排序
        • 2)快速排序
        • 3)归并排序
      • (2)二分
      • (3)高精度
        • a)高精度加法
          • 方法1
          • 方法2
        • b)高精度减法
          • 方法1
          • 方法2
        • c)高精度乘法
          • 双精度方法1
          • 双精度方法2
        • d)高精度除法
          • 方法1
          • 方法2
      • (4)前缀和差分
        • 1)前缀
            • 一维前缀和
            • 二维前缀和
        • 2)差分
            • 一维差分
            • 二维差分
      • (5)双指针算法
      • (6)位运算
          • **1.技巧一:用于消去x的最后一位的1**
            • **1.1.应用一 用O(1)时间检测整数n是否是2的幂次.**
            • **1.2.应用二 计算在一个 32 位的整数的二进制表示中有多少个 1**
          • 2.异或的运用
            • 2.1排除偶次
            • 2.2排除偶次变种
      • (7)离散化
      • (8)区间合并
      • (9)三分
    • 三、数据结构
      • (1)单链表(数组模拟)
      • (2)双链表(数组模拟)
      • (3)栈
      • (4)队列
      • (5)单调栈
      • (6)单调队列
      • (7)KMP
      • (8)Trie(字典树)
      • (9)并查集
      • (10)堆
      • (11)哈希表
    • 四、搜索与图论
      • (1)DFS(深度优先遍历算法)
      • (2)BFS(广度优先遍历算法)
      • (3)树与图的深度优先遍历
      • (4)树与图的广度优先遍历
      • (5)拓扑排序
      • (6)Dijkstra
      • (7)bellman-ford
      • (8)spfa
      • (9)Floyd
      • (10)Prim
      • (11)Kruskal
      • (12)染色法判定二分图
      • (13)匈牙利算法
    • 五、数学知识
      • (1)质数
        • 筛法
        • 1)朴素筛(埃氏筛)
        • 2)线性筛
      • (2)约数
      • (3)欧拉函数
      • (4)快速幂
          • 乘法逆元的定义
      • (5)扩展欧几里得算法
      • (6)中国剩余定理
      • (7)高斯消元
      • (8)求组合数
      • (9)容斥原理
      • (10)博弈论
    • 六、动态规划
      • (1)背包问题
      • (2)线性DP
      • (3)区间DP
      • (4)计数类DP
      • (5)数位统计DP
      • (6)状态压缩DP
      • (7)树形DP
      • (8)记忆化搜索
    • 七、 贪心
      • (1)区间问题
      • (2)Huffman树
      • (3)排序不等式
      • (4)绝对值不等式
      • (5)推公式
    • 八、STL
      • (1)vector容器
      • (2)队列,优先队列
      • (3)栈
      • (4)map容器
      • (5)优先队列和priority_queue
      • (6)set
      • (7)next_permutation
    • 九、由数据范围反推算法复杂度以及算法内容

一、常用函数及算法

(1)进制

1)十进制转R进制(2<= R<=36)

string DtoR(int n,int r)
{
	string s = "";
	int x;
	if(n == 0)
	{
		s = "0";
	}
	while(n)
	{
		x = n % r;
		n = n/r;
		if(x < 10)
		{
			s = char(x +48) + s;
		}
		else
		{
			s = char(x - 10 +'A') + s;
		}
	}
	return s;
}

2)R进制转十进制

int RtoD(string s, int r) // s为R进制数
{
    int len = s.size();
    int x, k = 0;
    for (int i = 0; i < len; i++)
    {
        if (isupper(s[i]))
        {
            x = s[i] - 'A' + 10;
        }
        else
        {
            x = s[i] - '0';
        }
        k = k * r + x;
    }
    return k; // 十进制数
}

(2)判断素数

方法1

//判断素数
int judge(int n)
{
	int i;
	if(n<=1) return 0;
	for(i = 2;i <= n/i;i++)//条件可改为i <= sqrt(n);
		if( n % i == 0) return 0;
	return 1;
}

方法2

//判断素数
int judge(int n)
{
	int i;
	if(n<=1) return 0;
	for(i = 2;i <= sqrt(n);i++)
	{
		if( n % i == 0)
		{
			return 0;
		}
	}
	return 1;
}

方法原理

正整数A,设它的平方根为Q
1、如果存在一个大于1小于Q的整数可以整除A,则必然存在一个大于Q小于A的整数可以整除A;
2、如果不存在一个大于1小于Q的整数可以整除A,则必然也不存在一个大于Q小于A的整数可以整除A。

先证明第1条
设B是一个大于1小于Q的整数,且B可以整除A。按整除的定义存在一个整数C,使得 B * C = A
C必然大于Q。因为如果C小于等于Q,则B * C <= B * Q < Q * Q = A。这与整除定义相矛盾。

再证明第2条
设C是一个整数,Q < C < A,且C可以整除A。按整除的定义存在一个整数B,使得 B * C = A
由于 Q < C 所以 B < Q(证明方法同上,不再赘述),这与题设2矛盾,所以这样的C不存在。

证毕

(3)字符串

将一行未知长度的字符串读入字符数组(一般用EOF,用换行符在很多oj里会出现段错误(segmentation fault))

	char num[100000];
	char c;
	int k = 0;
	while((c = getchar()) != EOF)
	{
		num[k++] = c;
	}

ASCII码转字符以及字符转ASCII码

	for(int i = 0;i < k;i++)
	{
		int nn = (int)num[i]+4; //字符转ASCII码
		cout<<(char)nn;//ASCII码转字符
	}

也可以直接对字符串中的某个字符进行ASCII码加减,然后直接输出相应字符

#include
using namespace std;

string s;

int main()
{
    getline(cin, s);//读入一行字符串
    int l = s.length();
    for(int i = 0; i < l; i++) s[i] += 4;//直接对字符串中的某个字符进行ASCII码加减
    cout << s;
    return 0;
}

(4)统计字母出现次数(C++)

使用map容器进行统计,方便快捷

#include
using namespace std;

int main()
{
	maps;//s是一个映射
	char c;
	do
	{
		cin>>c;
		if(isalpha(c))//判断是字母
		{
			c = tolower(c);//变成小写
			s[c]++;
		}
	}while(c != '#');
	//迭代器访问容器
	for(map::iterator it = s.begin();it != s.end();++it)
	{
		cout<first<<":"<second<

(5)常见ASCII码

  1. 数字0~9的ASCII对应值为 48 ~ 57
  2. 大写A ~Z的ASCII对应值为 65 ~ 90
  3. 小写a~z的ASCII对应值为 97 ~ 122
  4. 回车,ASCII码13
  5. 换行,ASCII码10
  6. 空格,ASCII码32

(6)字符串转整型

string s;
string res;
for(int i = 0;i < s.size();i++)
{
    res += s[i] - '0';
}

(7)整型转字符串

to_string()

reverse(s.begin(),s.end())

(8)ToLower实现

string ToLower(string s)
{
    string res=""; 
    for(int i=0;i < s.size();i++)
    {
    	if (s[i]>='A'&&s[i]<='Z')
    		res+=(s[i]+'a'-'A');
        else
            res+=s[i];
    return res;
}

(9)String中find函数的用法

//find 函数 返回jk 在s 中的下标位置    
position = s.find("jk");  
 if (position != s.npos)  //如果没找到,返回一个特别的标志c++中用npos表示,我这里npos取值是4294967295,   

//find 函数 返回flag 中任意字符 在s 中第一次出现的下标位置   
 flag = "c";  
 position = s.find_first_of(flag);  
 cout << "s.find_first_of(flag) is : " << position << endl;  

 //从字符串s 下标5开始,查找字符串b ,返回b 在s 中的下标
 position=s.find("b",5);
 cout<<"s.find(b,5) is : "<

(10)substr的用法

str.substr(i,len);//截取字符串str从下标i开始长度为len的子串

(11)字符串分割方法(以任意长度空格分割,注意只能以空格分割)

string str;
string s[1000];
getline(cin,str);
stringstream input(str);
string result;
int k = 0;
while(input >> result)
	s[k++] = result;

(12)x >> i & 1含义

判断x二进制表示中第i位是0还是1;

(13)运行时间复杂度分析

clock_t start,end;
start = clock();

end = clock();
cout<<(double)(end-start)/CLOCKS_PER_SEC<

(14)读取二维字符数组的方式(方便的一种)

char f[N][N];
for(int i = 0;i < n;i++)
		cin>>f[i];

二、基础算法

(1)排序

1)冒泡排序

void bubbleSort(int a[], int n)
{
    for(int i = n - 1; i > 0; i--)
        for(int j = 0; j < i; j++)
            if(a[j] > a[j+1]) 
                swap(a[j], a[j+1]);
}

2)快速排序

#include 

using namespace std;

const int N = 100010;

int q[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;

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

    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
    quick_sort(q, 0, n - 1);
    for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);

    return 0;
}

3)归并排序

利用分治的思想

#include 

using namespace std;

const int N = 1e5 + 10;

int a[N], tmp[N];

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;//如果数据小于等于1个直接退出

    int mid = l + r >> 1;//直接把最中间的值作为mid

    merge_sort(q, l, mid), merge_sort(q, mid + 1, r);//递归,不停的分治,压栈
    //最后一次递归结束后,会从后往前执行递归中还没有进行的剩余代码,用栈的知识理解为弹栈消失
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        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 (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    merge_sort(a, 0, n - 1);
    for (int i = 0; i < n; i ++ ) printf("%d ", a[i]);
    return 0;
}

(2)二分

模板1

	while (l < r)
    {
        int mid = l + r >> 1;	//(l+r)/2
        if (check(mid))  r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }

模板2

	while (l < r)
    {
        int mid = l + r + 1 >> 1;	//(l+r+1)/2
        if (check(mid))  l = mid;
        else r = mid - 1;
    }

第一个模板是尽量往左找目标,第二个模板是尽量往右找目标。

只要是往左找答案,就用第一个模板,mid不用加一,r=mid,l减一;
只要是往右找答案,就用第二个模板,mid要加一,l=mid,r要减一;

(3)高精度

a)高精度加法

方法1

不压位代码

#include 
#include 

using namespace std;

vector add(vector &A, vector &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t);
    return C;
}

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

    auto C = add(A, B);

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

    return 0;
}

压9位的代码

#include 
#include 

using namespace std;

const int base = 1000000000;

vector add(vector &A, vector &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % base);
        t /= base;
    }

    if (t) C.push_back(t);
    return C;
}

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

    for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (a[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            A.push_back(s);
            s = j = 0;
            t = 1;
        }
    }
    for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (b[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            B.push_back(s);
            s = j = 0;
            t = 1;
        }
    }

    auto C = add(A, B);

    cout << C.back();
    for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
    cout << endl;

    return 0;
}
方法2
#include
using namespace std;
const int N = 100100;

int a[N],b[N];
string s1,s2;
void init(string s,int *v)
{
    int n = s.size();
    reverse(s.begin(),s.end());
    v[0] = n;
    for(int i = 0;i < n;i++) v[i+1] = s[i] - '0';
}
vector add(int *a,int *b)
{
    vectorc;
    int t = 0,i = 0,x;
    //i 小于a和b 的长度
    while((i < a[0])||(i < b[0]))
    {
        i++;
        x = a[i] + b[i] + t;
        t = x / 10;
        c.push_back(x % 10);
    }
    if(t > 0) c.push_back(t);
    return c;
}
int main()
{
    cin>>s1>>s2;
    init(s1,a);
    init(s2,b);
    vectorres = add(a,b);
    reverse(res.begin(),res.end());
    for(int i = 0;i < res.size();i++) cout<

b)高精度减法

方法1
#include 
#include 

using namespace std;

bool cmp(vector &A, vector &B)
{
    if (A.size() != B.size()) return A.size() > B.size();

    for (int i = A.size() - 1; i >= 0; i -- )
        if (A[i] != B[i])
            return A[i] > B[i];

    return true;
}

vector sub(vector &A, vector &B)
{
    vector C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];//t = A[i]-B[i]-t;t是进位
        C.push_back((t + 10) % 10);//能解决A[i]比B[i]小的问题,也能解决A[i]比B[i]大的问题
        if (t < 0) t = 1;//t < 0说明需要进行借位,否则不需要
        else t = 0;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();//删除前导0
    return C;
}

int main()
{
    string a, b;
    vector 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');

    vector C;
	//通过cmp比较A和B的大小,让大位减去小位
    if (cmp(A, B)) C = sub(A, B);
    else C = sub(B, A), cout << '-';
	//逆序输出
    for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
    cout << endl;

    return 0;
}
方法2
#include
using namespace std;
const int N = 100010;
int a[N],b[N];
string s1,s2;


void init(string s,int *v)
{
    int n = s.size();
    v[0] = n;
    reverse(s.begin(),s.end());
    for(int i = 0;i < n;i++) v[i+1] = s[i]-'0';
}
vector sub(int *a,int *b)
{
    vectorc;
    int i = 0,x;
    while((i < a[0]) || (i < b[0]))
    {
        i++;
        x = a[i]-b[i];
        if(x < 0) x += 10,a[i+1]--;
        c.push_back(x);
    }
    while(c.size() > 1&&c.back() == 0) c.pop_back();
    return c;
}
int main()
{
    cin>>s1>>s2;
    if(s1.size() < s2.size()||(s1.size() == s2.size()&&s1 < s2))
    {
        cout<<"-";
        swap(s1,s2);
    }
    init(s1,a);
    init(s2,b);
    vectorres = sub(a,b);
    for(int i = res.size()-1;i >= 0;i--) cout<

c)高精度乘法

高精度数×低精度数

#include 
#include 

using namespace std;
vector mul(vector &A, int b)
{
    vector C;
    int t = 0;
    for (int i = 0; i < A.size() || t; i++)
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}

//高精度数×低精度数
int main()
{
    string a;
    int b;

    cin >> a >> b;

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

    auto C = mul(A, b);

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

    return 0;
}
双精度方法1

高精度数×高精度数

#include
using namespace std;

vector mul(vector &A,vector &B)
{
	vectorres;
	int p = 0;
	for(int j = 0;j < B.size();j++)
	{
		//cout< C;
		int t = 0;
		for(int i = 0;i < A.size()||t;i++)
		{
			if(i < A.size()) t += A[i]*B[j];
			C.push_back(t % 10);
			t /= 10;
		}
		int temp = p;
		if(p == 0)
		{
			for(long long i = 0;i < A.size()*B.size();i++)
				res.push_back(0);
		}
		int k = 0;
		for(int i = 0;i < C.size();i++)
		{
			//cout<<"C[i] = "< 1 && res.back() == 0) res.pop_back();

    return res;
}
int main()
{
	string a,b;
	cin>>a>>b;
	vector A;
	vector B;
	for(int i = a.size()-1;i >= 0;i--) A.push_back(a[i] - '0');
	for(int i = b.size()-1;i >= 0;i--) B.push_back(b[i] - '0');
	
	auto C = mul(A,B);
	for(int i = C.size()-1;i >= 0;i--) cout<
双精度方法2
#include
using namespace std;
const int N = 100010;
vectora,b;
string s1,s2;
void init(string s,vector &v)
{
    int n = s.size();
    reverse(s.begin(),s.end());
    for(int i = 0;i < n;i++) v.push_back(s[i]-'0');
}
vector mul(vectora,vectorb)
{
    vectorc(a.size()+b.size());
    for(int i = 0;i < a.size();i++)
        for(int j = 0;j < b.size();j++)
            c[i+j] += a[i]*b[j];
    int n = a.size() + b.size();
    for(int i = 0;i < n-1;i++)
    {
        c[i+1] += c[i] / 10;
        c[i] = c[i] % 10;
    }
    while(c.size() > 1&&c.back() == 0) c.pop_back();
    return c;
}

int main()
{
    cin>>s1>>s2;
    init(s1,a);
    init(s2,b);
    vectorres = mul(a,b);
    for(int i = res.size()-1;i >= 0;i--) cout<

d)高精度除法

方法1
#include 
#include 
#include 

using namespace std;
//A/B A是被除数,B 是除数,r 是余数
vector div(vector &A, int b, int &r)
{
    vector C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());//为了和其他三种运算相符合,可以一起进行运算,所以倒序存储
    while (C.size() > 1 && C.back() == 0) C.pop_back();//去除前导0
    return C;
}

int main()
{
    string a;
    vector A;

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

    int r;
    auto C = div(A, B, r);

    for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];//倒序输出

    cout << endl << r << endl;

    return 0;
}
方法2
#include
using namespace std;
const int N = 100010;
vectora;int b;
vectorc;
string s1,s2;
void init(string s,vector &v)
{
    int n = s.size();
    for(int i = 0;i < n;i++) v.push_back(s[i]-'0');
}
int div(vector a,int b,vector &c)
{
    int t = 0,x;//返回余数
    for(int i = 0;i < a.size();i++)
    {
        x = t * 10 + a[i];
        c.push_back(x / b );
        t = x % b;
    }
    reverse(c.begin(),c.end());
    while(c.size() > 1&&c.back() == 0) c.pop_back();
    return t;
    
}

int main()
{
    cin>>s1>>b;
    init(s1,a);
    int r = div(a,b,c);
    for(int i = c.size()-1;i >= 0;i--) cout<

(4)前缀和差分

1)前缀

一维前缀和
#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], s[N];//前缀和一般定义两个数组,一个为原数组,另一个为前n项和的数组。类比数列中的前n项和

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    //初始化前缀和数组
	//s[i]数组代表的是前i个数之和,初始化s[0] = 0,区间【l,r】之和为s[r]-s[l-1]
    for (int i = 1; i <= n; 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;
}
二维前缀和

子矩阵的和(画矩阵图理解)

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int s[N][N];

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

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

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];

    while (q -- )
    {
        int x1, y1, x2, y2;//当使用万能头时,不能写在main函数外面可能冲突造成编译错误
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }

    return 0;
}

以下为二维前缀和的矩阵解释,框中数字代表坐标

行和列的下标都从1开始,防止初始化数组时遇到麻烦,处理要写的代码比较多

1,1 1,2 1,3 1,4
2,1 2,2 2,3 2,4
3,1 3,2 3,3 ,3,4
4,1 4,2 4,3 4,4
5,1 5,2 5,3 5,4

其中a[i][j] 代表原数组中第 i 行第 j 列的元素z

前缀和的求法是先按行再按列进行计算,即第一行计算完转向第二行,可以通过遍历原数组实现

s [i] [j] = s [i-1] [j] + s [i] [j-1] - s [i-1] j-1] + a[i] [j];//减是因为s[i-1] [j-1]这一块被加过两次了

求(x1,y1),(x2,y2)区间内矩阵和为多少

ans = s[x2] [y2] - s[x1 - 1] [y2] - s[x2] [y1 - 1] + s[x1 - 1] [y1 - 1];//加是因为s[i-1] [j-1]这一块被减过两次了

2)差分

一维差分
#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

void insert(int l, int r, int c)
{
    b[l] += c;
    b[r + 1] -= c;
}

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

    for (int i = 1; i <= n; 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]);

    return 0;
}
二维差分

差分矩阵

#include 

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N];

void insert(int x1, int y1, int x2, int y2, int c)
{
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

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

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

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            insert(i, j, i, j, a[i][j]);

    while (q -- )
    {
        int x1, y1, x2, y2, c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, c);
    }

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];

    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
        puts("");
    }

    return 0;
}

(5)双指针算法

最长连续不重复子序列

#include 

using namespace std;

const int N = 100010;

int n;
int q[N], s[N];

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

    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;
        while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
        res = max(res, i - j + 1);
    }

    cout << res << endl;

    return 0;
}

数组元素的目标和

#include 

using namespace std;

const int N = 1e5 + 10;

int n, m, x;
int a[N], b[N];

int main()
{
    scanf("%d%d%d", &n, &m, &x);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);

    for (int i = 0, j = m - 1; i < n; i ++ )
    {
        while (j >= 0 && a[i] + b[j] > x) j -- ;
        if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl;
    }

    return 0;
}

判断子序列

#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
    for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);

    int i = 0, j = 0;
    while (i < n && j < m)
    {
        if (a[i] == b[j]) i ++ ;
        j ++ ;
    }

    if (i == n) puts("Yes");
    else puts("No");

    return 0;
}

(6)位运算

位运算基础
&
按位与
如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
|
按位或
两个相应的二进制位中只要有一个为1,该位的结果值为1
^
按位异或
若参加运算的两个二进制位值相同则为0,否则为1
~
取反
~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1
<<
左移
用来将一个数的各二进制位全部左移N位,右补0

右移
将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数, 高位补0

应用

1.技巧一:用于消去x的最后一位的1
x & (x-1)
x = 1100
x-1 = 1011
x & (x-1) = 1000
1.1.应用一 用O(1)时间检测整数n是否是2的幂次.

思路解析:N如果是2的幂次,则N满足两个条件。
1.N>0
2.N的二进制表示中只有一个1
一位N的二进制表示中只有一个1,所以使用N&(N-1)将唯一的一个1消去。
如果N是2的幂次,那么N&(N-1)得到结果为0,即可判断。

1.2.应用二 计算在一个 32 位的整数的二进制表示中有多少个 1

思路解析:
由 x & (x-1) 消去x最后一位知。循环使用x & (x-1)消去最后一位1,计算总共消去了多少次即可。

2.异或的运用
2.1排除偶次

示例:在一个整数数组中,仅存在一个不重复的数字,其余数字均出现两次(或偶数次),找出不重复数字。

// 异或方法:将所有整数异或,出现偶数次的整数会被抵消,最终留下不重复整数。
int result = 0;
for (int index = 0; index < numArray.length; index++) {
    result = result ^ numArray[index];
}
return result;
2.2排除偶次变种

示例:将数字1-1000存放在一个大小为1001的数组中,其中只有一个数字重复出现两次,找出重复数字。

除重复部分外,剩余部分均出现偶数个1,因此,整体异或为0值

因此,将1001个整数依次异或运算,最终结果就是重复的数字,相当于重复数字与0进行异或,得到其本身。

int result = 0;
for (int index = 0; index < numArray.length; index++) {
    result = result ^ numArray[index];
}
return result;

二进制中1的个数

#include 

using namespace std;

int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int x, s = 0;
        scanf("%d", &x);

        for (int i = x; i; i -= i & -i) s ++ ;

        printf("%d ", s);
    }

    return 0;
}

(7)离散化

区间和

#include 
#include 
#include 

using namespace std;

typedef pair PII;

const int N = 300010;

int n, m;
int a[N], s[N];

vector alls;
vector add, query;

int find(int 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;
}

vector::iterator unique(vector &a)
{
    int j = 0;
    for (int i = 0; i < a.size(); i ++ )
        if (!i || a[i] != a[i - 1])
            a[j ++ ] = a[i];
    // a[0] ~ a[j - 1] 所有a中不重复的数

    return a.begin() + j;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});

        alls.push_back(x);
    }

    for (int i = 0; i < m; i ++ )
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});

        alls.push_back(l);
        alls.push_back(r);
    }

    // 去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls), alls.end());

    // 处理插入
    for (auto item : add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];

    // 处理询问
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }

    return 0;
}

(8)区间合并

区间合并

#include 
#include 
#include 

using namespace std;

typedef pair PII;

void merge(vector &segs)
{
    vector res;

    sort(segs.begin(), segs.end());

    int st = -2e9, ed = -2e9;
    for (auto seg : segs)
        if (ed < seg.first)
        {
            if (st != -2e9) res.push_back({st, ed});
            st = seg.first, ed = seg.second;
        }
        else ed = max(ed, seg.second);

    if (st != -2e9) res.push_back({st, ed});

    segs = res;
}

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

    vector segs;
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        segs.push_back({l, r});
    }

    merge(segs);

    cout << segs.size() << endl;

    return 0;
}

(9)三分

#include
using namespace std;
const int N = 10000;
int n;
int arr[N];
int trisearch(int arr[],int n)
{
    if(arr[0] == '\0') return -1; 
    int left = 0;
    int right = n;
    while(left < right)
    {
        int midl = left + (right - left)/3;
        int midr = right - (right - left)/3;
        if(arr[midl] > arr[midr])
            right = midr - 1;
        else
            left = midl + 1;
    }

    return left;
}

int main()
{
    //求最大值
    int arr[] = {1,2,5,7,11,14,16,17,20,22,25,28,30,33,35,38,40,42,46,50,53,57,58,60, 55, 44, 33, 22, 11 , 10, 7, 6, 5, 1};
    int n = 34;
    int index = trisearch(arr,n);
    if(index == -1)
        cout<<-1<

三、数据结构

(1)单链表(数组模拟)

#include 

using namespace std;

const int N = 100010;


// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 将x插到头结点
void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}

// 将x插到下标是k的点后面
void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}

// 将下标是k的点后面的点删掉
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main()
{
    int m;
    cin >> m;

    init();

    while (m -- )
    {
        int k, x;
        char op;

        cin >> op;
        if (op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if (op == 'D')
        {
            cin >> k;
            if (!k) head = ne[head];
            else remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
    cout << endl;

    return 0;
}

(2)双链表(数组模拟)

#include
using namespace std;

const int N = 1e5+50;
int e[N],l[N],r[N],idx;

void init()
{
	r[0] = 1;
	l[1] = 0;
	idx = 2;
}
//在k的右边插入x
void add(int k,int x)
{
	e[idx] = x;//e用来存值,l,r数组用来存下标
	r[idx] = r[k];
	l[idx] = k;
	l[r[k]] = idx;
	r[k] = idx++;
}
//删除k节点
void remove(int k)
{
	l[r[k]] = l[k];
	r[l[k]] = r[k];
}
int main()
{
	init();
	int m;cin>>m;
	while(m--)
	{
		string s;
		int a = 0,b = 0;
		cin>>s;
		if(s == "L")
		{
			cin>>a;
			add(0,a);
		}
		else if(s == "R")
		{
			cin>>a;
			add(l[1],a);
		}
		else if(s == "D")
		{
			cin>>a;
			remove(a+1);//注意下标从2开始所以此处用a+1
		}
		else if(s == "IR")
		{
			cin>>a>>b;
			add(a+1,b);
		}
		else if(s == "IL")
		{
			cin>>a>>b;
			add(l[a+1],b);
		}
	}
	for(int i = r[0];i != 1;i = r[i]) cout<

(3)栈

#include
using namespace std;

const int N = 100100;
int stk[N],tt;
int m;

int main()
{
	cin>>m;
	while(m--)
	{
		string op;
		cin>>op;
		int num;
		if(op == "push") 
		{
			cin>>num;
			stk[++tt] = num;
		}
		else if(op == "pop")
		{
			tt--;
		}
		else if(op == "empty")
		{
			if(tt > 0) cout<<"NO"<

(4)队列

数组模拟队列
操作:出队,入队,返回队头元素,返回队列是否为空;

#include
using namespace std;

const int N = 100100;
int q[N],hh,tt = -1;//hh队头,tt队尾
int main()
{
    int m;cin>>m;
    while(m--)
    {
        string op;cin>>op;
        if(op == "push")
        {
            int x;cin>>x;
            q[++tt] = x;
        }
        else if(op == "pop")hh++;//从队头出队
        else if(op == "empty") 
        {
            cout<<(hh <= tt? "NO":"YES")<

(5)单调栈

#include
using namespace std;

//单调栈,栈中的元素始终是单调的
const int N = 100100;
int stk[N],tt;
int main()
{
    int n;cin>>n;
    while(n--)
    {
        int x;cin>>x;
        while(tt&&stk[tt] >= x) tt--;//当输入一个比左侧数字还小的数字时,会把栈中比输入的x大的数字全部删除
        if(tt) cout<

(6)单调队列

题目:滑动窗口

#include
using namespace std;

int n,k;
const int N = 1001000;
int a[N],q[N],hh,tt = -1;
int main()
{
    cin>>n>>k;
    for(int i = 0;i < n;i++) cin>>a[i];

    for(int i = 0;i < n;i++)
    {
        if(hh <= tt&&(i - k + 1) > q[hh]) hh++;
        //首先保证队列不为空。单调递增队列
        while(hh <= tt&&a[q[tt]] >= a[i]) tt--;
        q[++tt] = i;//单调队列中维护的是下标,下标对应的元素是单调的
        if(i -k + 1>= 0) cout< q[hh]) hh++;
        //首先保证队列不为空。单调递减队列
        while(hh <= tt&&a[q[tt]] <= a[i]) tt--;
        q[++tt] = i;//单调队列中维护的是下标
        if(i -k + 1>= 0) cout<

(7)KMP

参考博客:https://blog.csdn.net/v_JULY_v/article/details/7041827

题目:KMP字符串

#include
using namespace std;
const int N = 100100;
const int M = 1001000;
int n,m;
char s[M],p[N];
int ne[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
	cin>>n>>p+1>>m>>s+1;
	//求next数组
	for(int i = 2,j = 0;i <= n;i++)
	{
		while(j&&p[i] != p[j+1]) j = ne[j];
		if(p[i] == p[j+1]) j++;
		ne[i] = j;
	}
	//下标从1开始
	for(int i = 1,j = 0;i <= m;i++)
	{
		while(j&&s[i] != p[j+1])j = ne[j];
		if(s[i] == p[j+1]) j++;
		if(j == n)
		{
			cout<

(8)Trie(字典树)

字典树(又叫单词查找树、TrieTree),是一种树形结构,典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串,如01字典树)。主要思想是利用字符串的公共前缀来节约存储空间。很好地利用了串的公共前缀,节约了存储空间。字典树主要包含两种操作,插入和查找。

#include
using namespace std;

const int N = 100100;

int son[N][26],cnt[N],idx;

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]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[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;//没找到
        p = son[p][u];//在trie树中,p是唯一的
    }
    return cnt[p];
}

int main()
{
    int n;cin>>n;
    while(n--)
    {
        string s;cin>>s;
        if(s == "I") 
        {
            cin>>str;
            insert(str);
        }
        else 
        {
            cin>>str;
            cout<

(9)并查集

合并集合

#include
using namespace std;

const int N = 1000100;
int t[N],n,m;
int find(int x)//返回根节点+路径压缩;
{
    if(t[x] != x) t[x] = find(t[x]);
    return t[x];
}
int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++) t[i] = i;
    while(m--)
    {
        string s;
        int a,b;
        cin>>s>>a>>b;
        if(s == "M") t[find(a)] = find(b);
        else{
            if(find(a) == find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

连通块中点的数量

#include
using namespace std;

const int N = 100100;
int t[N],cnt[N];
int n,m;

int find(int x)
{
    if(t[x] != x) t[x] = find(t[x]);
    return t[x];
}
int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++)
    {
        t[i] = i;
        cnt[i] = 1;
    }
    while(m--)
    {
        string s;
        cin>>s;
        int a,b;
        if(s == "C")
        {
            cin>>a>>b;
            a = find(a),b =find(b);
            if(a != b)
            {
                t[a] = b;//这一步之后把a的集合插入b
                cnt[b] += cnt[a];//这里必须用原来的find(a),上一步后find(a)已改变
            }
        }
        else if(s == "Q1")
        {
            cin>>a>>b;
            if(find(a) == find(b))
                cout<<"Yes"<>a;
            cout<

(10)堆

利用完全二叉树实现

手写堆

#include
using namespace std;

const int N = 100100;
int n,m;
int h[N],cnt;

void down(int x)
{
    int u = x;//完全二叉树越往下越小
    if(x*2 <= cnt&&h[x*2] < h[u]) u = x * 2;//左子树比自己还小,则下标更新
    if((x*2+1) <= cnt&&h[x*2+1] < h[u]) u = x*2+1;//右子树比上一次比较中的较小者还小,则下标更新
    //找三个中的最小值
    if(x != u)
    {
        swap(h[x],h[u]);
        down(u);
    }
}

int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++) cin>>h[i];
    cnt = n;

    //建堆O(N)的建法
    for(int i = n/2;i;i--) down(i);
    //因为完全二叉树除了最后一层,前面有n/2的数

    while(m--)
    {
        cout<

模拟堆

#include
using namespace std;

const int N = 100100;
int h[N],ph[N],hp[N],cnt;

void heap_swap(int a,int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a],hp[b]);
    swap(h[a],h[b]);
}

void down(int u)
{
    int t = u;
    if(u*2 <= cnt&&h[u*2] < h[t]) t = u*2;
    if((u*2+1) <= cnt&&h[u*2+1] < h[t]) t = u*2+1;
    if(u != t)
    {
        heap_swap(u,t);
        down(t);
    }
}

void up(int u)
{
    while(u/2&&h[u] < h[u/2])
    {
        heap_swap(u,u/2);
        u >>= 1;//u除2然后不停循环,直到无法再上升
    }
}
int main()
{
    int n;cin>>n;
    int m = 0;
    while(n--)
    {
        string s;int k,x;
        cin>>s;
        if(s == "I")
        {
            cin>>x;
            cnt++;
            m++;
            ph[m] = cnt;hp[cnt] = m;//建立映射关系
            h[cnt] = x;
            up(cnt);
        }
        else if(s == "PM")
        {
            cout<>k;
            k = ph[k];
            heap_swap(k,cnt);
            cnt--;
            down(k);
            up(k);
        }
        else
        {
            cin>>k>>x;
            k = ph[k];
            h[k] = x;
            up(k);
            down(k);

        }
    }
    return 0;
}

(11)哈希表

哈希表又称散列表,散列函数,可以实现在O(1)的时间进行查找操作,这是将一个大范围映射到一个小范围中。

因此可能会出现冲突,即两个不同的数对应到同一个数上,为了避免这种情况,提出了两种方法,主要利用这两种方法解决。

1、开放寻址法

利用单个数组,映射到N的范围内,因为冲突概率比较低,并且冲突的数比较少,所以将数组的长度开成两倍(即2*N)

2、拉链法

有点像邻接图

模拟散列表

开放寻址法

#include
using namespace std;

const int N = 200003,null = 0x3f3f3f3f;//0x3f3f3f3f是无穷大
int h[N],n;
//开放寻址法
int find(int x)
{
    int t = (x%N + N) % N;
    while(h[t] != null&&h[t] != x)
    {
        t++;
        if(t == N) t = 0;
    }
    return t;
}
int main()
{
    cin>>n;
    memset(h,0x3f,sizeof h);//初始化h
    while(n--)
    {
        string s;
        int x;
        cin>>s>>x;
        if(s == "I") h[find(x)] = x;
        else{
            if(h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}

拉链法

#include
using namespace std;

const int N = 100003;
int h[N],e[N],ne[N],idx;
//h头节点,e存值,ne存下一个数的地址
void insert(int x)
{
    int k = (x % N + N)%N;
    //插入到链表中
    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])
    {
        if(e[i] == x) return true;
    }
    return false;

}
int main()
{
    int n;cin>>n;
    memset(h,-1,sizeof h);
    while(n--)
    {
        string s;int num;
        cin>>s>>num;
        if(s == "I") insert(num);
        else {
            if(find(num)) cout<<"Yes"<

四、搜索与图论

(1)DFS(深度优先遍历算法)

DFS需要用到递归和回溯

排列数字问题

方法1:

#include
using namespace std;

const int N = 10;

int n,path[N],st[N];

void dfs(int u)
{
    if(u == n)
    {
        for(int i = 0;i < n;i++)
        {
            cout<>n;
    dfs(0);
    return 0;
}

方法2

#include 

using namespace std;

const int N = 10;

int n;
int path[N];

void dfs(int u, int state)
{
    if (u == n)
    {
        for (int i = 0; i < n; i ++ ) printf("%d ", path[i]);
        puts("");

        return;
    }

    for (int i = 0; i < n; i ++ )
        if (!(state >> i & 1))
        {
            path[u] = i + 1;
            dfs(u + 1, state + (1 << i));
        }
}

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

    dfs(0, 0);

    return 0;
}

n-皇后问题

深搜,按行搜索

#include
using namespace std;

const int N = 20;

char g[N][N];
int n;
bool col[N],dg[N],udg[N];
//按行搜索
void dfs(int u)
{
    if(u == n)
    {
        for(int i = 0;i < n;i++) 
        {
            for(int j = 0;j < n;j++)
            {
                cout<>n;
    for(int i = 0;i < n;i++)
        for(int j = 0;j < n;j++)
            g[i][j] = '.';
    dfs(0);
    return 0;
}

按位深搜

#include
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool col[N],row[N],dg[N],udg[N];//主对角线,副对角线
//dg,udg下标存的是截距,副对角线y = x + b;b = y - x;为了使b大于0加一个偏移量n
//b = y - x + n;主对角线y = -x + b;b = x + y;
void dfs(int x,int y,int s)
{
    if (s > n) return;
    if(y == n) y = 0,x++;//当y走到边界时
    if(x == n)//x已经到最后一行了,x指行,y指列
    {
        if(s == n)
        {
            for(int i = 0;i < n;i++)
            {
                for(int j = 0;j < n;j++)
                {
                    cout<>n;
    for(int i = 0;i < n;i++)
        for(int j = 0;j < n;j++)
            g[i][j] = '.';
    dfs(0,0,0);

    return 0;
}

(2)BFS(广度优先遍历算法)

走迷宫

#include
using namespace std;

const int N = 110;
typedef pairPII;
int n,m;
int g[N][N],d[N][N];

//BFS
int bfs()
{
    queueq;
    memset(d,-1,sizeof d);
    q.push({0,0});
    d[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];
            //判断x,y是否越界
            if(x >= 0&&x < n&&y >= 0&&y < m&&g[x][y] == 0&&d[x][y] == -1)
            {
                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经典问题)

#include
using namespace std;

int bfs(string state)
{
    queue q;
    unordered_map d;
    q.push(state);
    d[state] = 0;
    string end = "12345678x";
    while(q.size())
    {
        auto t = q.front();
        q.pop();
        int dis = d[t];
        if(t == end) return dis;
        int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
        int k = t.find('x');
        int x = k / 3,y = k % 3;
        for(int i = 0;i < 4;i++)
        {
            int a = x + dx[i],b = y + dy[i];
            if(a >= 0&&a < 3&&b >=0&&b < 3)
            {
                swap(t[a*3+b],t[k]);
                if(!d.count(t))
                {
                    d[t] = dis+1;
                    q.push(t);
                }
                swap(t[a*3+b],t[k]);
            }
        }
    }
    return -1;
}
int main()
{
    string s;
    string state;
    for(int i = 0;i < 9;i++)
    {
        cin>>s;
        state += s;
    }
    cout<

(3)树与图的深度优先遍历

树的重心

#include
using namespace std;

const int N = 100100,M = N*2;
int n,e[M],h[M],ne[M],idx;
int ans  = N;
bool st[N];
void add(int a,int b)
{
	//在a后面插入b
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

int dfs(int u)
{
	st[u] = true;
	int sum = 0,size = 0;//sum代表每一个子树之和
	
	for(int i = h[u];i != -1;i = ne[i])
	{
		int j = e[i];
		if(st[j]) continue;//如果已经搜索过,则跳过
		
		int s = dfs(j);//一直搜到叶节点
		size = max(size,s);
		sum += s;
	}
	size = max(size,n - sum -1);//n-sum-1 --代表当除了当前节点以及当前节点的子树
	ans = min(ans,size);//最大值的最小值
	
	return sum+1;
	
}
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);
	cout<

(4)树与图的广度优先遍历

图中点的层次

#include
using namespace std;

const int N = 100100;
int n,m,d[N];
int h[N],e[N],ne[N],idx;

//用邻接表的方式存储图
void add(int a,int b)
{
    e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}

int bfs()
{
    queue q;
    memset(d,-1,sizeof d);
    q.push(1);
    d[1] = 0;
    while(q.size())
    {
        int 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;//j位于第几层,即为最短需要多远才能到
                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<

(5)拓扑排序

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

拓扑排序只针对有向图,如果图中存在环,则一定不存在拓扑序。

简单说就是排好序后所有节点都是由前面的节点指向后面的节点。

有向图的拓扑序列

#include
using namespace std;

const int N = 100100;

//利用邻接表存储图
int h[N],e[N],ne[N],idx;
int d[N];//存储所有点的入度数
int q[N];//存储最后排好序的序列
int n,m;

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

bool topsort()
{
    int hh = 0,tt = -1;
    for(int i = 1;i <= n;i++)//1-n个节点
        if(!d[i])
            q[++tt] = i;

    while(hh <= tt)
    {
        int t = q[hh++];//逻辑删除,实际上原来的数还在
        for(int i = h[t];i != -1;i = ne[i])//枚举t到j的每一条边
        {
            int j = e[i];
            d[j]--;
            if(d[j] == 0)//如果j的入度为0就可以加入到队列中,这样即满足拓扑排序
                q[++tt] = j;
        }
    }
    return tt == n-1;//tt == n-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]++;
    }
    if(topsort())
    {
        for(int i = 0;i < n;i++)
        {
            cout<

(6)Dijkstra

最短路算法归纳

  1. 单源最短路
    1. 所有边权为正数
      • 朴素版dijkstra(O(n*n))
      • 堆优化版dijkstra(O(mlogn))
    2. 存在负权边
      • Bellmen-Ford(O(nm))
      • SPFA(一般为O(m) 最坏为O(nm))
  2. 多源汇最短路
    • Floyd算法(O(n^3))

经典的最短路算法

朴素版dijkstra(O(n*n))

#include
using namespace std;

const int N = 510;
int g[N][N];//邻接矩阵存储
bool st[N];
int dist[N];
int n,m;

int dijkstra()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;

    for(int i = 0;i < n-1;i++)
    {
        int t = -1;
        for(int j = 1;j <= n;j++)
        {
            if(!st[j]&&(t == -1||dist[t] > dist[j]))
            {
                t = j;//找到离已确定点最小的距离的点
            }
        }

        for(int j = 1;j <= n;j++)
        {
            dist[j] = min(dist[j],dist[t]+g[t][j]);
        }
        st[t] = true;//代表t号点已经确定了

    }
    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(O(mlogn))

#include
using namespace std;

const int N = 1001000;//邻接表存储,因为邻接矩阵开不了那么大
//堆优化版
int n,m;
int h[N],e[N],w[N],ne[N],idx;//w为权重

int dist[N];
bool st[N];
typedef pair PII;

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;//设置1号节点权重为0,dist存储权重
    priority_queue,greater> heap;//默认是大根堆,此处为小根堆 根节点小于叶节点
    heap.push({0,1});//第一个点的权重为0

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

        int ver = t.second;int distance = t.first;
        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver];i != -1;i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];//选择权重更小的
                heap.push({dist[j],j});
            }
        }
    }
    if(dist[n] == 0x3f3f3f3f) return -1;
    else return dist[n];
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);//初始化h

    while(m--)
    {
        int a,b,c;cin>>a>>b>>c;
        add(a,b,c);
    }

    cout<

(7)bellman-ford

有边数限制的最短路

#include
using namespace std;

const int N = 510,M = 10010;
int dist[N],last[N];//last存上一次的dist
int n,m,k;


struct Edge{
    int a,b,w;
}edges[M];//m条边

int bellmen_ford()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;

    for(int i = 0;i < k;i++)
    {
        memcpy(last,dist,sizeof dist);
        for(int j = 0;j < m;j++)
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b],last[e.a]+e.w);
        }
    }

    if(dist[n] > 0x3f3f3f3f/2) return -1;
    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 = bellmen_ford();
    if(t == -1) cout<<"impossible"<

(8)spfa

spfa求最短路

属于bellmen_ford的优化版,和堆优化版的dijkstra算法求最短路很像

#include
using namespace std;

const int N = 100100;
int h[N],e[N],w[N],ne[N],idx;
int n,m;
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++;
}

int spfa()
{
    memset(dist,0x3f,sizeof dist);
    dist[1] = 0;
    queue q;
    q.push(1);
    st[1] = true;

    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t]  = false;
        for(int i = h[t];i != -1;i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t]+w[i];
                if(!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }

            }
        }
    }

    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);
    }
    int t = spfa();
    if(t == -1) cout<<"impossible"<

spfa判断负环

判断负环的原理:抽屉原理
dist[i] >= n;说明到达i经过n+1个点,实际上只有n个点,所以存在负环

#include
using namespace std;

const int N = 100100;
int h[N],e[N],w[N],ne[N],idx;
int n,m;
int dist[N],cnt[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++;
}

int spfa()
{

    queue q;
    //要判断有没有负环,所以把每个点都放进来,可能从1号点到不了负环
    for(int i = 1;i <= n;i++)
    {
        st[i] = true;
        q.push(i);
    }

    while(q.size())
    {
        int 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()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
    }

    if(spfa()) cout<<"Yes"<

(9)Floyd

Floyd求最短路

#include
using namespace std;

const int N = 1000,INF = 1e9;
int d[N][N];
int n,m,k;

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>>k;
    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,w;
        cin>>a>>b>>w;
        d[a][b] = min(d[a][b],w);
    }
    floyd();
    while(k--)
    {
        int x,y;
        cin>>x>>y;
        if(d[x][y] > INF/2) puts("impossible");
        else cout<

(10)Prim

Prim算法求最小生成树

#include
using namespace std;

const int N = 510,INF = 0x3f3f3f3f;

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

int prim()
{
    memset(dist,0x3f,sizeof dist);
    int res = 0;
    for(int i = 0;i < n;i++)
    {
        //先找到离集合最小的点,然后用这个点去更新其他点到集合的距离
        int t = -1;
        for(int j = 1;j <= n;j++)
            if(!st[j]&&(t == -1||dist[t] > dist[j]))
                t = j;
        if(i&&dist[t] == INF) return INF;
        if(i) res += dist[t];//如果不是第一点则将权重相加,这个点放入集合中
        //必须先把权重相加,再去更新其他点,因为在更新的过程中dist[t]可能会发生变化

        for(int j = 1;j <= n;j++)
            dist[j] = min(dist[j],g[t][j]);
        st[t] = true;//记录t已经加入集合了
    }
    return res;
}
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<<"impossible"<

(11)Kruskal

Kruskal算法求最小生成树

#include
using namespace std;

const int N = 1001000,INF = 0x3f3f3f3f;

int n,m;
int p[N];

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 kruskal()
{
    sort(edges,edges+m);
    //初始化并查集
    for(int i = 1;i <= n;i++)p[i] = i;

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

        a = find(a),b = find(b);
        if(a != b)
        {
            p[a] = b;
            res += w;
            cnt++;
        }
    }
    if(cnt < n-1) return INF;
    else return res;
}
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};
    }

    int t = kruskal();
    if(t == INF) puts("impossible");
    else cout<

(12)染色法判定二分图

染色法判定二分图

#include
using namespace std;

const int N = 100100,M = 200200;

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"<

(13)匈牙利算法

二分图的最大匹配

#include
using namespace std;

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

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

bool find(int x)
{
    //遍历所有与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);
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }   
    int res = 0;
    for(int i = 1;i <= n1;i++)
    {
        memset(st,false,sizeof st);//这里一定要初始化,即使是已经匹配过的,也可以再选然后递归
        //查看是否可以让原匹配的点进行调换,如果可以,那么也算匹配成功
        if(find(i)) res++;
    }
    cout<

五、数学知识

(1)质数

筛质数

筛法

1)朴素筛(埃氏筛)

#include 
#include 

using namespace std;

const int N= 1000010;

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

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}

2)线性筛

#include 
#include 

using namespace std;

const int N= 1000010;

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

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}

(2)约数

试除法求约数

#include
using namespace std;

vector get_divisors(int n)
{
    vector res; 
    for(int i = 1;i <= n / i;i++)
    {
        if(n % i == 0)
        {
            res.push_back(i);
            if(i != n / i) res.push_back(n / i); 
        }

    }
    sort(res.begin(),res.end());
    return res;
}
int main()
{
    int x;
    cin>>x;
    while(x--)
    {
        int a;
        cin>>a;
        auto ans = get_divisors(a);

        for(auto t : ans) cout<

约数个数

#include
using namespace std;

const int mod = 1e9+7;
typedef long long LL;

int main()
{
    int n;
    cin>>n;
    unordered_mapprimes;
    while(n--)
    {
        int x;
        cin>>x;
        for(int i = 2;i <= x/i;i++)
        {
            while(x % i == 0)
            {
                x /= i;
                primes[i] ++;
            }
        }
        if(x > 1) primes[x]++;
    }
    LL res = 1;
    for(auto prime:primes) res = res*(prime.second+1)%mod;  
    cout<

约数之和

#include
using namespace std;

const int mod = 1e9+7;
typedef long long LL;

int main()
{
    int n;
    cin>>n;
    unordered_mapprimes;
    while(n--)
    {
        int x;
        cin>>x;
        for(int i = 2;i <= x/i;i++)
        {
            while(x % i == 0)
            {
                x /= i;
                primes[i] ++;
            }
        }
        if(x > 1) primes[x]++;
    }
    LL res = 1;

    for(auto prime:primes)
    {
        LL t = 1;
        int p = prime.first,a = prime.second;
        while(a--) t = (t*p + 1) % mod;
        res = res * t %mod;
    }
    cout<

最大公约数

#include
using namespace std;

int gcd(int a,int b)
{
    return b?gcd(b,a%b):a;
}
int main()
{
    int n;cin>>n;
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        cout<

(3)欧拉函数

欧拉函数

#include
using namespace std;
//欧拉函数的定义:1-N中与N互质的数的个数
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int x;
        cin>>x;
        int res = x;

        for(int i = 2;i <= x/i;i++)
            if(x % i == 0)
            {
                res = res/i*(i-1);//等价于res*(1-1/i),1/i可能是小数所以通分
                while(x % i == 0) x /= i;
            }
        if(x > 1) res = res/x*(x-1);

        cout<

筛法求欧拉函数

#include
using namespace std;

typedef long long LL;
const int N  =1000100;
bool st[N];
int primes[N];
int phi[N];
int cnt;
LL get_ou(int n)
{
    phi[1] = 1;

    for(int i = 2;i <= n;i++)
    {
        if(!st[i])
        {
            primes[cnt++] = i;
            phi[i] = i-1;//质数直接就是i-1个
        }
        for(int j = 0;primes[j] <= n/i;j++)
        {
            st[primes[j]*i] = true; 
            if(i % primes[j] == 0)
            {
                phi[primes[j]*i] = primes[j]*phi[i];
                break;
            }
            phi[primes[j]*i] = phi[i]*(primes[j]-1);
        }
    }
    LL res = 0;
    for(int i = 1;i <= n;i++) res += phi[i];
    return res;
}
int main()
{
    int n;
    cin>>n;

    cout<

(4)快速幂

快速幂

给定 n组 ai,bi,pi,对于每组数据,求出 a(下标为i,上标为bi)mod pi值。

#include
using namespace std;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

int n;
typedef long long LL;

LL qmi(int a,int k,int p)
{
    LL res = 1;
    while(k)
    {
        if(k & 1) res = (LL)res*a % p;
        k >>= 1;
        a = (LL)a*a % p;
    }
    return res;
}
int main()
{
    IOS;
    cin>>n;
    while(n--)
    {
        int a,k,p;
        cin>>a>>k>>p;
        cout<

快速幂求逆元

给定 n组 ai,pi,其中 pi是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出 impossible

注意:请返回在 0∼p−10∼p−1 之间的逆元。

乘法逆元的定义

若整数 b,m互质,并且对于任意的整数 a,如果满足 b|a,则存在一个整数 x,使得 a/b≡a×x(modm),则称 x 为 b 的模 m乘法逆元,记为 b−1(mod m)。
b 存在乘法逆元的充要条件是 b 与模数 m 互质。当模数 m 为质数时,bm−2 即为 b 的乘法逆元。

#include 
#include 

using namespace std;

typedef long long LL;


LL qmi(int a, int b, int p)
{
    LL res = 1;
    while (b)
    {
        if (b & 1) res = res * a % p;
        a = a * (LL)a % p;
        b >>= 1;
    }
    return res;
}


int main()
{
    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int a, p;
        scanf("%d%d", &a, &p);
        if (a % p == 0) puts("impossible");
        else printf("%lld\n", qmi(a, p - 2, p));
    }

    return 0;
}

(5)扩展欧几里得算法

#include
using namespace std;

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

}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int a,b,x,y;
        cin>>a>>b;
        exgcd(a,b,x,y);
        cout<

线性同余方程

#include
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
using namespace std;
typedef long long ll;
int exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
        x = 1,y = 0;
        return a;
    }
    int d = exgcd(b,a%b,y,x);
    y -= a / b *x;
    return d;

}
int main()
{
    IOS;
    int n;
    cin>>n;
    while(n--)
    {
        int a,b,m,x,y;
        cin>>a>>b>>m;
        int d = exgcd(a,m,x,y);
        //最后求的是x
        if(b % d) puts("impossible");
        else cout<< (ll)x*(b/d)%m <

(6)中国剩余定理

表达整数的奇怪方式

给定 2n 个整数 a1,a2,…,an 和 m1,m2,…,mn,求一个最小的非负整数 x,满足 ∀i∈[1,n],x≡mi(mod ai)。

#include 
#include 

using namespace std;

typedef long long LL;


LL exgcd(LL a, LL b, LL &x, LL &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }

    LL d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}


int main()
{
    int n;
    cin >> n;

    LL x = 0, m1, a1;
    cin >> m1 >> a1;
    for (int i = 0; i < n - 1; i ++ )
    {
        LL m2, a2;
        cin >> m2 >> a2;
        LL k1, k2;
        LL d = exgcd(m1, m2, k1, k2);
        if ((a2 - a1) % d)
        {
            x = -1;
            break;
        }

        k1 *= (a2 - a1) / d;
        k1 = (k1 % (m2/d) + m2/d) % (m2/d);

        x = k1 * m1 + a1;

        LL m = abs(m1 / d * m2);
        a1 = k1 * m1 + a1;
        m1 = m;
    }

    if (x != -1) x = (x % m1 + m1) % m1;

    cout << x << endl;

    return 0;
}

(7)高斯消元

高斯消元解线性方程组

#include 
#include 
#include 

using namespace std;

const int N = 110;
const double eps = 1e-6;

int n;
double a[N][N];


int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i < n + 1; i ++ ) swap(a[t][i], a[r][i]);
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];

        for (int i = r + 1; i < n; i ++ )
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2;
        return 1;
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] -= a[j][n] * a[i][j];

    return 0;
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n + 1; j ++ )
            cin >> a[i][j];

    int t = gauss();

    if (t == 0)
    {
        for (int i = 0; i < n; i ++ ) printf("%.2lf\n", a[i][n]);
    }
    else if (t == 1) puts("Infinite group solutions");
    else puts("No solution");

    return 0;
}

高斯消元解异或线性方程组

输入一个包含 nn 个方程 nn 个未知数的异或线性方程组。

方程组中的系数和常数为 00 或 11,每个未知数的取值也为 00 或 11。

求解这个方程组。

#include 
#include 

using namespace std;

const int N = 110;


int n;
int a[N][N];


int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )
            if (a[i][c])
                t = i;

        if (!a[t][c]) continue;

        for (int i = c; i <= n; i ++ ) swap(a[r][i], a[t][i]);
        for (int i = r + 1; i < n; i ++ )
            if (a[i][c])
                for (int j = n; j >= c; j -- )
                    a[i][j] ^= a[r][j];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (a[i][n])
                return 2;
        return 1;
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] ^= a[i][j] * a[j][n];

    return 0;
}


int main()
{
    cin >> n;

    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n + 1; j ++ )
            cin >> a[i][j];

    int t = gauss();

    if (t == 0)
    {
        for (int i = 0; i < n; i ++ ) cout << a[i][n] << endl;
    }
    else if (t == 1) puts("Multiple sets of solutions");
    else puts("No solution");

    return 0;
}

(8)求组合数

求组合数 I

给定 n 组询问,每组询问给定两个整数 a,b,请你输出Cabmod(10e9+7) 的值。

#include 
#include 

using namespace std;

const int N = 2010, mod = 1e9 + 7;


int c[N][N];


void init()
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}


int main()
{
    int n;

    init();

    scanf("%d", &n);

    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);

        printf("%d\n", c[a][b]);
    }

    return 0;
}

求组合数 II

#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, mod = 1e9 + 7;


int fact[N], infact[N];


int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int main()
{
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i ++ )
    {
        fact[i] = (LL)fact[i - 1] * i % mod;
        infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    }


    int n;
    scanf("%d", &n);
    while (n -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);
    }

    return 0;
}

求组合数 III

给定 n 组询问,每组询问给定三个整数 a,b,p,其中 p 是质数,请你输出 Cab mod p 的值。

#include 
#include 

using namespace std;

typedef long long LL;


int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int C(int a, int b, int p)
{
    if (b > a) return 0;

    int res = 1;
    for (int i = 1, j = a; i <= b; i ++, j -- )
    {
        res = (LL)res * j % p;
        res = (LL)res * qmi(i, p - 2, p) % p;
    }
    return res;
}


int lucas(LL a, LL b, int p)
{
    if (a < p && b < p) return C(a, b, p);
    return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}


int main()
{
    int n;
    cin >> n;

    while (n -- )
    {
        LL a, b;
        int p;
        cin >> a >> b >> p;
        cout << lucas(a, b, p) << endl;
    }

    return 0;
}

求组合数 IV

输入 a,b,求 Cab 的值。

注意结果可能很大,需要使用高精度计算。

#include 
#include 
#include 

using namespace std;


const int N = 5010;

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


void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p)
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector mul(vector a, int b)
{
    vector c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;
}


int main()
{
    int a, b;
    cin >> a >> b;

    get_primes(a);

    for (int i = 0; i < cnt; i ++ )
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p);
    }

    vector res;
    res.push_back(1);

    for (int i = 0; i < cnt; i ++ )
        for (int j = 0; j < sum[i]; j ++ )
            res = mul(res, primes[i]);

    for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
    puts("");

    return 0;
}

满足条件的01序列

给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。

输出的答案对 10e9+7 取模

#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, mod = 1e9 + 7;


int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int main()
{
    int n;
    cin >> n;

    int a = n * 2, b = n;
    int res = 1;
    for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;

    for (int i = 1; i <= b; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;

    res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;

    cout << res << endl;

    return 0;
}

(9)容斥原理

能被整除的数

给定一个整数 n和 m 个不同的质数 p1,p2,…,pm。

请你求出 1∼n中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

#include 
#include 

using namespace std;

typedef long long LL;

const int N = 20;

int p[N];


int main()
{
    int n, m;
    cin >> n >> m;

    for (int i = 0; i < m; i ++ ) cin >> p[i];

    int res = 0;
    for (int i = 1; i < 1 << m; i ++ )
    {
        int t = 1, s = 0;
        for (int j = 0; j < m; j ++ )
            if (i >> j & 1)
            {
                if ((LL)t * p[j] > n)
                {
                    t = -1;
                    break;
                }
                t *= p[j];
                s ++ ;
            }

        if (t != -1)
        {
            if (s % 2) res += n / t;
            else res -= n / t;
        }
    }

    cout << res << endl;

    return 0;
}

(10)博弈论

Nim游戏

给定 n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

#include 
#include 

using namespace std;

const int N = 100010;


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

    int res = 0;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

台阶-Nim游戏

现在,有一个 nn 级台阶的楼梯,每级台阶上都有若干个石子,其中第 ii 级台阶上有 aiai 个石子(i≥1i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

#include 
#include 

using namespace std;

const int N = 100010;

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

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        if (i & 1) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

集合-Nim游戏

给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 110, M = 10010;

int n, m;
int s[N], f[M];


int sg(int x)
{
    if (f[x] != -1) return f[x];

    unordered_set S;
    for (int i = 0; i < m; i ++ )
    {
        int sum = s[i];
        if (x >= sum) S.insert(sg(x - sum));
    }

    for (int i = 0; ; i ++ )
        if (!S.count(i))
            return f[x] = i;
}


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

    memset(f, -1, sizeof f);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

拆分-Nim游戏

给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

#include 
#include 
#include 
#include 

using namespace std;

const int N = 110;


int n;
int f[N];


int sg(int x)
{
    if (f[x] != -1) return f[x];

    unordered_set S;
    for (int i = 0; i < x; i ++ )
        for (int j = 0; j <= i; j ++ )
            S.insert(sg(i) ^ sg(j));

    for (int i = 0;; i ++ )
        if (!S.count(i))
            return f[x] = i;
}


int main()
{
    cin >> n;

    memset(f, -1, sizeof f);

    int res = 0;
    while (n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

六、动态规划

(1)背包问题

1.01背包问题

01背包,二维数组

#include
using namespace std;

const int N = 1010;

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

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]);
            }
    int res = 0;
    //n件物品,容量为i的价值的最大值
    for(int i = 0;i <= m;i++) res = max(res,f[n][i]);
    cout<

一维数组优化

#include
using namespace std;

const int N = 1010;

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

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]);//从后往前相当于都是以f[i-1][?]开始的
    cout<

2.完全背包问题

背包中数量无限多,与01背包题面上最大的区别

二维

#include
using namespace std;

const int N = 1010;
int v[N],w[N];
int dp[N][N];
int n,m;
//二维
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++)
            {
                dp[i][j] = dp[i-1][j];
                if(j >= v[i])
                {
                    dp[i][j] = max(dp[i-1][j] ,dp[i][j-v[i]] + w[i]);
                }
            }
    cout<

一维

#include
using namespace std;

const int N = 1010;
int v[N],w[N];
int n,m;
int dp[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++)
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
    cout<

3.多重背包问题

#include
using namespace std;

const int N = 1010;
int v[N],w[N],s[N];
int dp[N];
int n,m;
//二维  暴力
int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++) cin>>v[i]>>w[i]>>s[i];

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

4.多重背包问题 II

#include
using namespace std;

const int N = 2010;
int n,m;
int dp[N];
//二进制优化,按2的整次幂打包,对于每一个方案就是01背包问题
int v[N],w[N],s[N];

struct Good{
    int v,w;
};
int main()
{
    vectorgoods;
    cin>>n>>m;
    for(int i = 1;i <= n;i++) cin>>v[i]>>w[i]>>s[i];

    for(int i = 1;i <= n;i ++)
    {
        for(int k = 1;k <= s[i];k *= 2)
        {
            s[i] -= k;
            goods.push_back({k*v[i],k*w[i]});   
        }
        if(s[i] > 0) goods.push_back({s[i]*v[i],s[i]*w[i]});
    }
    for(auto good : goods)
    {
        for(int j = m;j >= good.v;j--)
        {
            dp[j] = max(dp[j],dp[j-good.v] + good.w);
        }
    }
    cout<

5.分组背包问题

#include
using namespace std;

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

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

(2)线性DP

1.数字三角形

状态表示:由最后一层到当前点的权重最大值

#include
using namespace std;


const int N = 510;

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= i;j++)
            cin>>w[i][j];

    //对f数组初始化
    for(int i = 1;i <= n;i++) f[n][i] = w[n][i];

    for(int i = n-1;i;i--)
        for(int j = 1;j <= i;j++)
            f[i][j] = max(f[i+1][j],f[i+1][j+1])+ w[i][j];
    cout<

2.最长上升子序列

#include
using namespace std;

const int N = 1010;
int a[N],f[N];
int n;

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    for(int i = 1;i <= n;i++)cin>>a[i];

    for(int i = 1;i <= n;i++)
    {
        f[i] = 1;
        for(int j = 1;j < i;j++)
        {
            if(a[i] > a[j])
            {
                f[i] = max(f[i],f[j]+1);
            }
        }
    }
    int res = 0;
    for(int i = 1;i <= n;i++) res = max(res,f[i]);
    cout<

3.最长上升子序列II

#include
using namespace std;

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

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

    int len = 0;
    for(int i = 0;i < n;i++)
    {
        int l = 0,r = len;
        while(l < r)
        {
            int m = l + r + 1>> 1;
            if(q[m] < a[i]) l = m;
            else r = m-1; 
        }
        len = max(len,r+1);
        q[r+1] = a[i];//q[i]存的是长度为i的上升子序列的最后一个值的最小值
    }
    cout<

4.最长公共子序列

#include
using namespace std;

const int N = 1010;
int f[N][N];
char a[N],b[N];
int n,m;

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

5.最短编辑距离

#include
using namespace std;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);

const int N = 1010;
char a[N],b[N];
int n,m,f[N][N];

int main()
{
    IOS;
    cin>>n>>a+1>>m>>b+1;

    //初始化
    for(int i = 1;i <= m;i++) f[0][i] = i;
    for(int i = 1;i <= n;i++) f[i][0] = i;

    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
        {
            f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
            if(a[i]==b[j]) f[i][j] = min(f[i][j],f[i-1][j-1]);
            else f[i][j] = min(f[i][j],f[i-1][j-1]+1);  
        }   
    cout<

6.编辑距离

#include
using namespace std;

const int N = 15,M = 1010;

int n,m;
int f[N][N];
char str[M][N];

int edit(char a[],char b[])
{
    int la = strlen(a+1),lb = strlen(b+1);

    for(int i = 0;i <= la;i++) f[i][0] = i;
    for(int i = 0;i <= lb;i++) f[0][i] = i;

    for(int i = 1;i <= la;i++)
        for(int j = 1;j <= lb;j++)
        {
            f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
            f[i][j] = min(f[i][j],f[i-1][j-1]+(a[i] != b[j]));
        }
    return f[la][lb];
}
int main()
{
    cin>>n>>m;
    for(int i = 0;i < n;i++) cin>>(str[i]+1);

    while(m--)
    {
        int lim;
        char s[N];
        cin>>s+1>>lim;

        int res = 0;
        for(int i = 0;i < n;i++)
        {
            if(edit(str[i],s) <= lim)
                res++;
        }
        cout<

(3)区间DP

石子合并

设有 N堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

#include
using namespace std;

const int N = 310;

int f[N][N];
int s[N];
int n;

int main()
{
    cin>>n;
    for(int i = 1;i <= n;i++) cin>>s[i],s[i] += s[i-1];

    for(int len = 2;len <= n;len++)
    {
        for(int i = 1;i + len - 1 <= n;i++)
        {
            int j = i + len - 1;
            f[i][j] = 2e8;
            for(int k = i;k < j;k++)
            {
                f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
            }
        }
    }
    cout<

(4)计数类DP

整数划分

一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。

我们将这样的一种表示称为正整数 n的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

完全背包问题的思路

#include
using namespace std;

const int N = 1010,mod = 1e9+7;

int f[N];
int n;
//完全背包的思路
//f[i][j]表示只从1~i中选,且总和等于j的方案数
//状态转移方程:f[i][j] = f[i-1][j]+f[i][j-i]
int main()
{
    cin>>n;
    f[0] = 1;

    for(int i = 1;i <= n;i++)
        for(int j = i;j <= n;j++)
            f[j] = (f[j]+f[j-i])%mod;
    cout<

特殊的思路

#include
using namespace std;

const int N = 1010,mod = 1e9+7;

int f[N][N];
int n;

//f[i][j]指和为i,有j种方案数
//f[i][j] = f[i-1][j-1]+f[i-j][j];
int main()
{
    cin>>n;
    f[0][0] = 1;
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= i;j++)
            f[i][j] = (f[i-1][j-1]+f[i-j][j])%mod;
    int res = 0;
    //所有方案数求和
    for(int i = 1;i <= n;i++) res = (res + f[n][i])%mod;
    cout<

(5)数位统计DP

计数问题

给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。

例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…

#include 
#include 
#include 

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector num, int l, int r)
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x)
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;

    vector num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- )
    {
        if (i < n - 1)
        {
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);
        }

        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
    }

    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);

        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }

    return 0;
}

(6)状态压缩DP

蒙德里安的梦想

求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。

例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。

朴素写法,1000ms

#include 
#include 
#include 

using namespace std;

const int N = 12, M = 1 << N;

int n, m;
long long f[N][M];
bool st[M];

int main()
{
    while (cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            st[i] = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) st[i] = false;
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (int k = 0; k < 1 << n; k ++ )
                    if ((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];

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

去除无效状态的优化写法,230ms

#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 12, M = 1 << N;

int n, m;
LL f[N][M];
vector state[M];
bool st[M];

int main()
{
    while (cin >> n >> m, n || m)
    {
        for (int i = 0; i < 1 << n; i ++ )
        {
            int cnt = 0;
            bool is_valid = true;
            for (int j = 0; j < n; j ++ )
                if (i >> j & 1)
                {
                    if (cnt & 1)
                    {
                        is_valid = false;
                        break;
                    }
                    cnt = 0;
                }
                else cnt ++ ;
            if (cnt & 1) is_valid = false;
            st[i] = is_valid;
        }

        for (int i = 0; i < 1 << n; i ++ )
        {
            state[i].clear();
            for (int j = 0; j < 1 << n; j ++ )
                if ((i & j) == 0 && st[i | j])
                    state[i].push_back(j);
        }

        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ )
            for (int j = 0; j < 1 << n; j ++ )
                for (auto k : state[j])
                    f[i][j] += f[i - 1][k];

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

    return 0;
}

最短Hamilton路径

给定一张 n 个点的带权无向图,点从 0∼n−1标号,求起点 0 到终点 n−1的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

#include
using namespace std;

const int N = 20,M = 1 << N;

int w[N][N],f[M][N];
int n;

int main()
{
    cin>>n;
    for(int i = 0;i < n;i++)
        for(int j = 0;j < n;j++)
            cin>>w[i][j];
    memset(f,0x3f,sizeof f);
    f[1][0] = 0;//初始化,起点为0
    for(int i = 0;i < 1 << n;i++)
        for(int j = 0;j < n;j++)
            if(i >> j & 1)
            {
                for(int k = 0;k < n;k++)
                {
                    if(i >> k&1)
                    {
                        f[i][j] = min(f[i][j],f[i-(1<

(7)树形DP

没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

#include
using namespace std;

const int N = 10010;
int happy[N],f[N][2];
int e[N],ne[N],h[N],idx;
bool has_father[N];
int n;

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

void dfs(int u)
{
    f[u][1] = happy[u];
    for(int i = h[u];i != -1;i = ne[i])
    {
        int j = e[i];
        dfs(j);//找到每一个树的叶节点,然后回溯求值
        f[u][0] += max(f[j][0],f[j][1]);
        f[u][1] += f[j][0];
    }
}
int main()
{
    cin>>n;
    for(int i = 1;i <= n;i++) cin>>happy[i];

    memset(h,-1,sizeof h);
    for(int i = 0;i < n-1;i++)
    {
        int a,b;
        cin>>a>>b;//b是a 的父节点
        has_father[a] = true;
        add(b,a);
    }
    int root = 1;
    while(has_father[root]) root++;

    dfs(root);
    //f[root][0]不选根节点,f[root][1]选根节点
    cout<

(8)记忆化搜索

滑雪

给定一个 RR 行 CC 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 ii 行第 jj 列的点表示滑雪场的第 ii 行第 jj 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

 1  2  3  4 5

16 17 18 19 6

15 24 25 20 7

14 23 22 21 8

13 12 11 10 9

在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。


#include
using namespace std;

const int N = 310;
int h[N][N];
int f[N][N];
int n,m;

int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
int dp(int x,int y)
{
    int &v = f[x][y];//取地址
    if(v != -1) return v;
    v = 1;
    for(int i = 0;i < 4;i++)
    {
        int a = x + dx[i],b = y + dy[i];
        if(a >= 1&&a <= n&&b >= 1&&b <= m&&h[x][y] > h[a][b])
            v = max(v,dp(a,b)+1);
    }
    return v;
}
int main()
{
    cin>>n>>m;
    for(int i = 1;i <= n;i++)
        for(int j = 1; j <= m;j++)
            cin>>h[i][j];
    memset(f,-1,sizeof f);

    int res = 0;
    for(int i = 1;i <= n;i++)
        for(int j = 1;j <= m;j++)
            res = max(res,dp(i,j));
    cout<

七、 贪心

(1)区间问题

区间选点

给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    printf("%d\n", res);

    return 0;
}

最大不相交区间数量

给定 N 个闭区间[ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (ed < range[i].l)
        {
            res ++ ;
            ed = range[i].r;
        }

    printf("%d\n", res);

    return 0;
}

区间分组

给定 N个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。

#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }

    sort(range, range + n);

    priority_queue, greater> heap;
    for (int i = 0; i < n; i ++ )
    {
        auto r = range[i];
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
        else
        {
            heap.pop();
            heap.push(r.r);
        }
    }

    printf("%d\n", heap.size());

    return 0;
}

区间覆盖

给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 −1−1。

#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    int st, ed;
    scanf("%d%d", &st, &ed);
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }

    sort(range, range + n);

    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i ++ )
    {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st)
        {
            r = max(r, range[j].r);
            j ++ ;
        }

        if (r < st)
        {
            res = -1;
            break;
        }

        res ++ ;
        if (r >= ed)
        {
            success = true;
            break;
        }

        st = r;
        i = j - 1;
    }

    if (!success) res = -1;
    printf("%d\n", res);

    return 0;
}

(2)Huffman树

合并果子

在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过 n−1n−1 次合并之后,就只剩下一堆了。

达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。

假定每个果子重量都为 11,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。

例如有 33 种果子,数目依次为 1,2,91,2,9。

可以先将 1、21、2 堆合并,新堆数目为 33,耗费体力为 33。

接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 1212,耗费体力为 1212。

所以达达总共耗费体力=3+12=15=3+12=15。

可以证明 1515 为最小的体力耗费值。

每次都挑最小的两堆,小根堆优先队列实现

#include
using namespace std;

int n;
const int N = 10000+50;
priority_queue,greater>f;//小根堆从小到大

int main()
{
    //freopen("P1090_2.in","r",stdin);
    //freopen("P1090_2.out","w",stdout);
    cin>>n;
    for(int i = 1;i <= n;i++) 
    {
        int x;cin>>x;
        f.push(x);
    }

    long long res = 0;
    while(f.size() != 1)
    {
        int t1 = f.top();
        f.pop();
        int t2 = f.top();
        f.pop();
        res += t1 + t2;
        f.push(t1+t2);
    }
    cout<

(3)排序不等式

有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int t[N];

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

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

    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += t[i] * i;

    printf("%lld\n", res);

    return 0;
}

(4)绝对值不等式

货仓选址

在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

#include 
#include 

using namespace std;

const int N = 100010;

int n;
int q[N];

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

    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    sort(q, q + n);

    int res = 0;
    for (int i = 0; i < n; i ++ ) res += abs(q[i] - q[n / 2]);

    printf("%d\n", res);

    return 0;
}

(5)推公式

耍杂技的牛

农民约翰的 N 头奶牛(编号为 1…N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这 NN 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

#include 
#include 

using namespace std;

typedef pair PII;

const int N = 50010;

int n;
PII cow[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int s, w;
        scanf("%d%d", &w, &s);
        cow[i] = {w + s, w};
    }

    sort(cow, cow + n);

    int res = -2e9, sum = 0;
    for (int i = 0; i < n; i ++ )
    {
        int s = cow[i].first - cow[i].second, w = cow[i].second;
        res = max(res, sum - s);
        sum += w;
    }

    printf("%d\n", res);

    return 0;
}

八、STL

(1)vector容器

vector容器能存放任何类型的对象

功能 例子 说明
赋值 a.push_back(100); 在尾部添加元素
元素个数 int size = a.size(); 元素个数
是否为空 bool isEmpty = a.empty(); 判断是否为空
打印 cout< 打印第一个元素
中间插入 a.insert(a.begin()+i, k); 在第i个元素前面插入k
尾部插入 a.push_back(8); 尾部插入值为8的元素
尾部插入 a.insert(a.end(), 10,5); 尾部插入10个值为5的元素
删除尾部 a.pop_back(); 删除末尾元素
删除区间 a.erase(a.begin()+i, a.begin()+j); 删除区间[i, j-1]的元素
删除元素 a.erase(a.begin()+2); 删除第3个元素(线性时间)
调整大小 a.resize(n) 数组大小变为n
清空 a.clear();
翻转 reverse(a.begin(), a.end()); 用函数reverse翻转数组
排序 sort(a.begin(), a.end()); 用函数sort排序,从小到大

(2)队列,优先队列

主要操作

例子 说明
queue q; 定义栈,Type为数据类型,如int,float,char等
q. push(item); 把item放进队列
q.front(); 返回队首元素,但不会删除
q.pop(); 删除队首元素
q.back(); 返回队尾元素
q.size(); 返回元素个数
q.empty(); 检查队列是否为空

(3)栈

主要操作

例子 说明
stack< Type > stk; 定义栈,Type为数据类型,如int,float,char等。
stk.push(item); 把item放到栈顶。
stk.top(); 返回栈顶的元素,但不会删除。
stk.pop(); 删除栈顶的元素,但不会返回。
stk.size(); 返回栈中元素的个数。
stk.empty(); 检查栈是否为空,如果为空返回true,否则返回false。

(4)map容器

•map:关联容器,实现从键(key)到值(value)的映射。

•map效率高的原因:用平衡二叉搜索树来存储和访问。

•还存在unordered_map,插入和查找效率更高

(5)优先队列和priority_queue

•优先队列:优先级最高的先出队。

•队列和排序的完美结合,不仅可以存储数据,还可以将这些数据按照设定的规则进行排序。

•每次的push和pop操作,优先队列都会动态调整,把优先级最高的元素放在前面。

•优先队列一般可以替代堆

(6)set

•set:集合,集合中的元素是唯一的(去重)。

•STL的set用二叉搜索树实现,集合中的每个元素只出现一次,且是排好序的。访问元素的时间复杂度是O(logn)的。

•其次,也存在muti_set,unordered_set,都非常实用,可以自行了解

•set和map在竞赛题中应用很广泛。特别是需要用二叉搜索树处理数据的题目,如果用set或map实现,能极大地简化代码。

主要操作

例子 说明
set A; 定义
A. insert(item); 把item放进set
A.erase(item); 删除元素item
A.clear(); 清空set
A.empty (); 判断是否为空
A.size(); 返回元素个数
A.find(k); 返回一个迭代器,指向键值k
A.lower_bound(k); 返回一个迭代器,指向键值不小于k的第一个元素
A.upper_bound(); 返回一个迭代器,指向键值大于k的第一个元素

(7)next_permutation

•next_permutation(): 求“下一个”排列组合。

•例如三个字符{a, b, c}组成的序列,next_permutation()能按字典序返回6个组合:abc,acb,bac,bca,cab,cba。

•函数next_permutation()的定义有两种形式:

–bool next_permutation (BidirectionalIterator first, BidirectionalIterator last);

–bool next_permutation (BidirectionalIterator first, BidirectionalIterator last, Compare comp);

•返回值:如果没有下一个排列组合,返回false,否则返回true。每执行next_permutation()一次,会把新的排列放到原来的空间里。

•复杂度:O(n)。

•排列的范围:[first, last),包括first,不包括last。

#include 
using namespace std; 
string s; 

int main(){ 
	cin>>s; 
	do 
	{ 
		cout<

九、由数据范围反推算法复杂度以及算法内容

一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 107∼108107∼108 为最佳。

下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

    n≤30n≤30, 指数级别, dfs+剪枝,状态压缩dp
    n≤100n≤100 => O(n3)O(n3),floyd,dp,高斯消元
    n≤1000n≤1000 => O(n2)O(n2),O(n2logn)O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
    n≤10000n≤10000 => O(n∗n√)O(n∗n),块状链表、分块、莫队
    n≤100000n≤100000 => O(nlogn)O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分
    n≤1000000n≤1000000 => O(n)O(n), 以及常数较小的 O(nlogn)O(nlogn) 算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O(nlogn)O(nlogn) 的做法:sort、树状数组、heap、dijkstra、spfa
    n≤10000000n≤10000000 => O(n)O(n),双指针扫描、kmp、AC自动机、线性筛素数
    n≤109n≤109 => O(n√)O(n),判断质数
    n≤1018n≤1018 => O(logn)O(logn),最大公约数,快速幂
    n≤101000n≤101000 => O((logn)2)O((logn)2),高精度加减乘除
    n≤10100000n≤10100000 => O(logk×loglogk),k表示位数O(logk×loglogk),k表示位数,高精度加减、FFT/NTT

你可能感兴趣的:(ACM&OI,算法,c++)