ACM学习笔记

ACM学习笔记

随时思考或者思路简录

  1. n>10^5, 定义为全局变量

  2. 绝对值函数:abs() 向上取整函数:ceil()

  3. 1LL*的用法

  4. 可以使用并查集来判断一个图中是否存在环

  5. 初始化数组最大值 memset(a,0x3f,sizeof(a))

  6. 数字最大 const int maxn=0x3f3f3f3f;

  7. 不开 long long 见祖宗

  8. 字符串变数组 atoi(s)

  9. 求开n次方?pow(x,1.0/n) 首先x>0,x y太大用 double ,double的范围是10的308次方

文章目录

0.acm输入的几种情况:

1.输入包含若干组整数a,b,a,b之间通过空格隔开,每一组数据占一行。

while(scanf("%d %d",&a,&b)!=EOF){
}

2.第一行为一个整数N,紧接着是N行数据,每行包含有空格隔开的两个整数a,b,每组数据占一行

scanf("%d",&N);
while(N--){
	scanf("%d %d",&a,&b);//读入每组需要处理的数据
}

3.输入若干组整数a,b,a b之间通过空格隔开,每组数据占一行。当输入为0 0时,输入结束,本组数据不需要处理。

while(scanf("%d %d",&a,&b)&&(a!=0||b!=0)){
}

4.输入包含若干组测试数据。每组数据只占一行,每行包含一个整数N及其后紧跟着N个整数。以0开头的测试数据表示输入结束,本组数据不需要处理。

while(scanf("%d %d",&a,&b)&&N){
    while(N--){
	    scanf("%d",&element);
    }
}

1.c++STL

1.string

  1. 读一行:getline(cin,名字);
  2. memset(str,0,sizeof(str)) 暴力清空 逐个字节清空填充 也可初始化-1
  3. 字符串的长度 s.length()
  4. 首尾s.begin(),s.end()

2.ctime

time(0) 1970到现在的秒数

//srand(timo(0));

#include<ctime>
srand(unsigned int)time(NULL));//随机数种子
int random=rand()%61+40;

3.sort排序

  1. 头文件 algorithm

  2. **sort 可以排序 数组 字符串string(字典序) 结构体 vector **

  3. sort(数组名+n1,数组名+n2) 左闭右开 从小到大

  4. sort(数组名+n1,数组名+n2,greater<T>) 从大到小 T是数据类型

  5. sort(数组名+n1,数组名+n2,排序规则结构名())

struct 结构名
{
	bool operator()(const T & a1,const T & a2)const {
		//a1应该在a2前,返回 true
        //否则返回 false 
	}
}

​ 6. sort函数的bool写法。甚至可以给结构体排序

sort(a,a+n,cmp);

bool cmp(xuesheng a,xuesheng b){
   if(a.sum ==b.sum ) return a.k <b.k ;
   else return a.sum >b.sum ;
}

4.二分查找lower_bound

排好序的数组查找规则必须和排序规则一致

  1. binary_search(数组名+n1,数组名+n2,值) - 数组名
    查找等于(!a>b&&!a<b—a排在b前后都行)值的元素,找到返回true,否则false
  2. lower_bound(数组名+n1,数组名+n2,值,排序函数bool cmp ) - 数组名 二分查找下界 (大于等于值) 返回值是一个指针 可以减去 数组名+n1 得到下标。找不到即 数组名+n2。
  3. upper_bound(数组名+n1,数组名+n2,值,排序函数bool cmp) - 数组名 二分查找上界 即 大于值

5.全排类函数

  1. next_permutation()

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

  3. 先用sort()排序,得到最小排列后,然后再执行next_permutation()。

#include <bits/stdc++.h>
using namespace std;
int main(){
    string s="bca"; 
    sort(s.begin(),s.end());  //字符串内部排序,得到最小的排列“abc”
    do{ 
       cout<<s<<endl;
    }while(next_permutation(s.begin(),s.end()));
    return 0;
} 

  1. 如果序列中有重复元素,next_permutation()生成的排列会去重。例如“aab”,代码输出3个排列{aab, aba, baa},不是6个排列。

  2. 自写全排列函数 --深度优先搜索

#include<bits/stdc++.h>
using namespace std;
int a[20] = {1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20];         //记录第i个数是否用过
int b[20];            //生成的一个全排列
void dfs(int s,int t){
    if(s == t) {      //递归结束,产生一个全排 列
  //if(s == 3) {     //递归结束,取3个数产生一个排列
        for(int i = 0; i < t; ++i)  //i<3
            cout << b[i] << " ";    //输出一个排列            
        cout<<endl;
        return;
    }
    for(int i=0;i<t;i++)
        if(!vis[i]){
            vis[i] = true;   
            b[s] = a[i];
            dfs(s+1,t);
            vis[i] = false;
        }
}
int main(){
    int n = 3;
    dfs(0,n);     //前n个数的全排列
    return 0;
}

6.unipue函数

  1. include<algorithm>
  2. sort先排序 unipue后去重
  3. 返回不重复个数 len=unipue(s,s+12) - s ;

7.数组vector

  1. 加入 v.push_back 删除 v.popback() 清空v.clear
  2. n=v.size()
  3. 可以直接 用下标访问
  4. 迭代器访问for( auto i:v ) cout<<i;

8.集合set

  1. 自动排序+去重
  2. 迭代器遍历
  3. 函数:
image-20220731070507989

9.映射map

multiset的排序容器

#include<bits/stdc++.h>
#include<set>//使用multiset set 需要使用此头文件
using namespace std;

int main (){
	multiset<int> st;
    int a[10]={ 1,14,12,13,7,13,21,19,8,8};
    for(int i=0;i<10;i++){
		st.inset(a[i])//插入的是a[i]的复制品 
    }
    multiset<int>::iterator i;//迭代器 类似于指针
    for( i = st.begin();i!=st.end();++i){
		cout<<*i<<",";
    }
    cout<<endl;
    //输出1 7 8 8 12 13 13 14 19 21 
}

10.链表list

11.栈和队列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qEWdrAyH-1663738769822)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220731095946294.png)]

12 查找find

2.动态规划

【带备忘录的递归】

最长公共子序列

int dp[maxn][maxn];
void init(int n, int m) {//初始化 
    for(int i = 0; i < n; i++) { 
        dp[i][0] = 0; 
    }
    for(int i = 0; i < m; i++) { 
        dp[0][i] = 0; 
    } 
}
void LCS(string s1, string s2) { 
    int n = s1.length(), m = s2.length();//两个字符串从下标为1开始存 
    init(n, m); 
    for(int i = 1; i < n; i++) {//每一位遍历 
        for(int j = 1; j < m; j++) { 
            if(s1[i] == s2[j]) { 
                dp[i][j] = dp[i - 1][j - 1] + 1; 
            }
            else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); 
            } 
        } 
    } 
}

最长上升子序列

dp[i] 表示长度为 iLIS 的结尾元素

int len;//LIS的长度 
int s[maxn];//存储原始序列 
int dp[maxn];

void init() {//初始化 
    //序列第一位直接放进去就行 
    len = 1; 
    dp[len] = s[1]; 
}

void LIS() {//从下标为1开始存储数据 
    init();
    for(int i = 2; i <= n; i++) {//n为所给序列长度 
        if(s[i] > dp[len]) {//如果比当前LIS最后一位要大就直接接上去 
            dp[++len] = s[i]; 
        }
        else {//否则就修改数据 
            int pos = lower_bound(dp + 1, dp + len + 1, s[i]) - dp; 
            dp[pos] = s[i]; 
        }
    }
}

最大递增子序列和

#include<cstdio>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
//max,min,swap,unique
//dp[i]:以i结尾的数字可构成的最大和 
 
//   dp[i] = max(num[i] , dp[j:(1~i-1)] + num[i])   	num[i] > num[j]
 
int num[1000+5];
int dp[1005];		//表示以 第i个数字 结尾的子串的最大和 
 
int main()
{
	int n;
	while (~scanf ("%d",&n) && n)
	{
		for (int i = 1 ; i <= n ; i++)
			scanf ("%d",&num[i]);
		
		int ans = -INF;
		for (int i = 1 ; i <= n ; i++)
		{
			dp[i] = num[i];
			for (int j = 1 ; j < i ; j++)
			{
				if ( num[j]<num[i] )
					dp[i] = max(dp[i] , dp[j] + num[i]);
					ans = max(ans , dp[i]);
			}
		}
		printf ("%d\n",ans);
	}
	return 0;
}

3.快速幂

1.a^b%c 如何求?

#include<iostream>
using namespace std;

long long ksm(long long a,long long b,long long c){
    long long ans=1;
    while(b>0){
		if(b&1){//相当于b%2==1
            ans = ans * a %c;
        }
        a=a * a %c;
        b>>=1;// 相当于b=b/2;
    }
    return ans;
}

int main(){
	
	int a=3,b=3,c=25;
	cout<<ksm(a,b,c);

} 

4.记忆化搜索

以斐波那契数列为例

int F(int n){
    if(n<2)f[n]=1;          //这里f[]是储存数据的数组
    else if(f[n]==0)        //这里是重点
        f[n]=F(n-1)+F(n-2);
    return f[n];
}

注释:当f[n]没被计算过,就计算一次。也就是说,如果f[n]已经被计算过储存起来了,那就直接用储存的数据,便不用再计算了。

5.高精度

1.加法

方法:按位加再进位

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

string add(string sa,string sb)
{
	int lena=sa.size(),lenb=sb.size(),ma=max(lena,lenb);
	int a[1005]={0},b[1005]={0},c[1005]={0};
	for(int i=0;i<lena;i++) a[i]=sa[lena-i-1]-'0';
	for(int i=0;i<lenb;i++) b[i]=sb[lenb-i-1]-'0';
	for(int i=0;i<ma;i++){
		c[i]+=a[i]+b[i];
		if(c[i]>9) c[i+1]=c[i]/10,c[i]%=10;
	}
	if(c[ma]!=0) ma++;
	string s="";
	for(int i=ma-1;i>=0;i--) s+=char(c[i]+'0');
	return s;
}	
	
int main()
{
	string a,b;
	while(cin>>a>>b){
		cout<<add(a,b)<<endl;
	}
	return 0;
}

2.减法

#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main(){
	//计算 a - b = ans
	int i,j,flag=0; //flag用来记录是否为负数, 1代表负数, 默认为0即正数 
	string a_s,b_s; //用字符串接受两个数字 
	int a[521]={0},b[521]={0},ans[521]={0}; //三个存数字数组, 记得初始化为0 
	cin>>a_s>>b_s; //接受输入的字符串型 a 和 b 存到 a_s 和 b_s 里 
	
	int len_a=a_s.length(),len_b=b_s.length(); //计算 a 和 b 的长度 
	int len=len_a>len_b?len_a:len_b; //计算 a 和 b 的最大长度, 确定做减法时的循环次数 
	
	for(i=0;i<len_a;i++) a[i]=a_s[len_a-1-i]-'0'; //将字符型数字转化为整形数字 
	for(i=0;i<len_b;i++) b[i]=b_s[len_b-1-i]-'0'; //注意要倒序存储, 后面输出的时候再倒一遍 
	
	if(len_a>len_b||len_a==len_b&&a[len_a-1]>=b[len_b-1]){ //当 a 大于 b 时 
		for(i=0;i<len;i++){
			if(a[i]<b[i]){ //当对应位 a < b 时, 向高位借位 
				a[i+1]--; //高位减1 
				a[i]+=10; //这一位加10 
			}
			ans[i]=a[i]-b[i]; //做减法 
		}
	}
	else{ //当 a 小于 b 时, a - b 为负数, 所以变为 b - a, 保证减法结果为正 
		flag=1; //负数标记 
		for(i=0;i<len;i++){
			if(b[i]<a[i]){ //借位
				b[i+1]--; 
				b[i]+=10;
			}
			ans[i]=b[i]-a[i];
		}
	}
	
	if(flag) cout<<"-"; //输出负号 
	
	while(ans[len]==0&&len>0) len--; //去掉前置无用的 0 
	
	for(i=len;i>=0;i--){ //倒序输出结果 
		cout<<ans[i];
	}
}

3.乘法

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

string mul(string sa,string sb)
{
	int lena=sa.size(),lenb=sb.size(),lanab=lena+lenb;
	int a[1005]={0},b[1005]={0},c[1005]={0};
	for(int i=0;i<lena;i++) a[i]=sa[lena-i-1]-'0';
	for(int i=0;i<lenb;i++) b[i]=sb[lenb-i-1]-'0';
	
 for(int i = 0; i < lena; i++){
        for(int j = 0; j < lenb; j++){
            c[i + j] += a[i] * b[j];//按位相乘并累加
        }
    }
    int x = 0;
    for(int i = 0; i < lanab; i++){//处理进位
        c[i] += x;
        x = c[i]/10;
        c[i] %= 10;
    }
    
	int k = lanab,flag=0;//两数相乘后的位数不会超过两数位数相加之和,所以c1足够了
    while(c[k]==0 && k > 0){//处理前缀0
        k--;
        flag++;
    }
    int ma=lanab-flag+1;
	string s="";
	for(int i=ma-1;i>=0;i--) s+=char(c[i]+'0');
	return s;
}	
	
int main()
{	
	string a,b;
	while(cin>>a>>b){
		cout<<mul(a,b)<<endl;
	}
	return 0; 
}

4.除法

1.高精除低精
#include<iostream>
#include<cstdio>
#include<string>
#include<string.h>
#include<cmath>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f
#define maxn 5e5+5
//高精度除以低精度	

char s1[500005];//高精度 
ll b;     //低精度 
int a[500005];
int ans[500005];

void solve(){
	memset(a,0,sizeof(a));
	memset(ans,0,sizeof(ans));
	
	//scanf("%s %lld",s1,&b);
	int n=strlen(s1);
	for(int i=0;i<n;i++) a[i]=s1[i]-'0';
	
	
	
	ll pos=0;//存储余数 
	for(int i=0;i<n;i++){
		ans[i]=(pos*10+a[i])/b;
		pos=(pos*10+a[i])%b;
	}
	int len=0;// 记录前导零长度
	while(ans[len]==0&&len<n-1) ++len;
	 
	//for(int i=len;i<n;++i) printf("%d",c[i]);
	printf("%lld\n",pos);
}


int main()
{	
	
	while(~scanf("%s %lld",s1,&b))
	solve();

	return 0; 
}


2.高精除高精
#include<iostream>
#include<cstdio>
#include<string>
#include<string.h>
#include<cmath>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f
#define maxn 5e5+5
//高精度除以高精度	

string s1,s2;//这里用string存,会更加方便
int a[500005];
int b[500005];
int c[500005];
int tmp[500005];
void sub(string x,string y) //传入相减的字符串长度
 { 	memset(a,0,sizeof(a)); 
	memset(b,0,sizeof(b)); 
	memset(tmp,0,sizeof(tmp)); 

	int n = x.size(); 
	int m = y.size(); 

	for(int i=0;i<n;++i) a[n-i-1] = x[i] - '0'; 
	for(int i=0;i<m;++i) b[m-i-1] = y[i] - '0'; 

	int len = max(n,m); 
	for(int i=0;i<len;++i) { 
		if(a[i]<b[i]) 
		{ 
			a[i+1] -= 1; //向上借位
			a[i] += 10; //向上接了一位,故+10 
		} 
		tmp[i] = a[i] - b[i]; 
	} 

	while(tmp[len]==0&&len!=0) 
	--len; 

	//这里注意要更新s1的值 
	string ans = ""; 
	for(int i=len;i>=0;--i) ans += tmp[i] + '0'; 

	s1 = ans; 
 }
 int cmp(string s1,string s2)
 { /* 
s1>s2 返回1 
s1==s2 返回0 
s1<s2 返回-1 
*/ 
	int n = s1.size(); 
	int m = s2.size(); 
	if(n<m) return -1; 
	else if(n==m&&s1==s2) return 0; 
	else if(n==m&&s1<s2) return -1; 
	return 1; 
}
 void div()
 { cin>>s1>>s2; 

	memset(c,0,sizeof(c)); 
	int n = s1.size(); 
	int m = s2.size(); 

	int len = n - m; //商的位数 

	if(len<0) //特别处理 
	{ 
		printf("0\n"); 
		return; 
	} 

	for(int i=len;i>=0;--i) 
	{ 
		memset(b,0,sizeof(b)); 
		int pos = i; //需要添加0的个数 
		string ans = s2; //临时字符串变量 
		while(pos--) ans += '0'; 

		while(cmp(s1,ans)>=0) { 
			++c[i]; 
			sub(s1,ans); //高精度除法 
		} 
	} 
	
	while(c[len]==0&&len!=0) --len;  
	for(int i=len;i>=0;--i) printf("%d",c[i]); 

 }
 
 
 int main(){ 
 	div(); 
	return 0; 
 }

6.手动二分

//给出单调递增的整数序列 ,查找第一个大于或等于 的数的位置。
while(l<r){ 
    int mid=(l+r)>>1;//l表示区间左端点,r表示区间右端点 
    if(a[mid]>=x)//x为目标值 
        r=mid; 
    else l=mid+1;
}
//区间[l,r)

//另一种写法
while(l<r){
    int mid=(l+r+1)>>1;//l表示区间左端点,r表示区间右端点 
    if(a[mid]>=x) //x为目标值
        r=mid-1; 
    else l=mid;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2zL0JFc-1663738769823)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220302130717123.png)]

//上下为 左右之差别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rc4mB4lb-1663738769823)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220302130742401.png)]

7.尺取

//给定一个正数序列,求满足和大于或等于S的子序列(连续)的最短长度
while(1){
    while(r<n&&sum+a[r]<s){//
        sum+=a[r]; 
        r++;
    }
    if(r==n)break; 
    ans=min(ans,r-l+1);//
    sum-=a[l]; //
    l++;
}

//第二种写法
int l=r=1,sum=0.ans=100001;
while(1){
    sum+=a[r++];//右端点右移
    if(sum>=s){
        while(sum>=s) sum-=a[l++];//左端点右移
        ans=min(ans,r-l+1);
    }
    if(r==n+1) break;//到头了 
}  
   


8.并查集

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。

并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集

(根节点作为集合的代表元素),我们只要找到了某个元素的的树根,就能确定它在

哪个集合里。

多少个根节点 就有多少个集合

  1. 注意:还可以让花费最小的当祖先
const int MAXN = 1005;
int fa[MAXN]; 
void init() {//初始化
    for (int i=1; i<=MAXN; i++) {//1开始 不过这无关紧要
        fa[i]=i; 
    }
}

int find(int x) { //找x的祖先
    if (fa[x]==x) 
        return x;
    else 
        fa[x]=find(fa[x]);
    	return fa[x]; // 在查询根节点的过程中,进行路径压缩 
}

void union(int u,int v) { //u的祖先是v
    int u_fa =  find(u);//找到u的祖先
    int v_fa =  find(v);//找到v的祖先
    fa[u_fa] = v_fa; //uv的祖先合并
}

按秩合并
void merge(inr i,inr j){
	int x=find(i);
    int y=find(j);
    if(rank[x]<=rank[y]) fa[x]=y;
    else fa[y]=x;
    if(rank[x]==rank[y]&&x!=y) rank[y]++;
}

9.倍增RMQ(区间最值查询)

rmp [ i ] [ j ]表示i开始 2的 j 次方的最小值

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<string.h>
#include<cmath>
#include<vector>
#include<stack>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f

int n,q;
int rmqmin[50005][16];
int rmqmax[50005][16];
int lo[50005];
//初始化
void rmqinit(){
    for (int j = 1; (1<<j) <= n; ++j) 
    	for (int i = 1; i + (1 << j) - 1 <= n; ++i){
			 rmqmax[i][j] = max(rmqmax[i][j - 1], rmqmax[i + (1 << j - 1)][j - 1]);
             rmqmin[i][j] = min(rmqmin[i][j - 1], rmqmin[i + (1 << j - 1)][j - 1]);
		 }
}

//调用
int rmi(int l,int r){
    //int k=log2(l-r+1)
	int k=lo[r-l+1];
    return min(rmqmin[l][k],rmqmin[r-(1<<k)+1][k]);
}

int rma(int l,int r){
    //int k=log2(l-r+1)
	int k=lo[r-l+1];
    return max(rmqmax[l][k],rmqmax[r-(1<<k)+1][k]);
}
int main(){ 
	cin>>n>>q;
	//构造
	for(int i=1;i<=n;i++){
		cin>>rmqmax[i][0];
  	  	rmqmin[i][0]=rmqmax[i][0];
	}
	lo[1]=0;
	for(int i=2;i<=n;i++){
		lo[i]=lo[i/2]+1;//打表log2的值
	}
	rmqinit();
	while(q--){
		int l,r;
		cin>>l>>r;
		cout<< rma(l,r)-rmi(l,r) <<endl;
	}

	return 0; 
}

10.图和树的存储和遍历

链式前向星存图
  1. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-doE4tXK3-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220706192431064.png)]
  2. 链式前向星高效存图:head数组下标是节点,数组存的其实 是“第几条边”。
  3. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nl0SQlQB-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220805065338218.png)]
  4. 编号i123456
    起点uCABACC
    终点vACCBBD
    边权w416412
    下一条边的编号next-1-1-1215
#include<iostream>
#include<cstring>
using namespace std;

struct E{ 
    int to;
    int w;
    int next;
}edge[100]; 

int head[100],tot;//图 head是什么?

void init() {    //初始化图
    tot=1; 
	memset(head,-1,sizeof(head)); 
}

void addedge(int a,int b,int w)
{ //添加一条a到b边权为w的有向边,无向边就正反各加一次
    edge[tot].to=b; 
	edge[tot].w=w; 
	edge[tot].next=head[a]; 
	head[a]=tot++; //增加一个边
}

//遍历点x的连的出边 


int main(){
	init();
	addedge(1,2,3);
	addedge(1,4,4);
	addedge(1,3,1);
	addedge(2,5,2);
	addedge(4,5,5);
	addedge(5,3,6);
//	addedge(5,1,7);
	
	cout<<"tot="<<tot<<endl;
	
	for(int i=1;i<=tot;i++){
		cout<<head[i]<<endl;
	}
	
	
	int x=1;
	for(int i=head[x];i!=-1;i=edge[i].next) {
  	  int to=edge[i].to;
  	  int w=edge[i].w;
  	  printf("点x到点%d,边权为%d的边\n",to,w);
	}
	return 0;
}



邻接矩阵 存图
  1. 空间复杂度高

  2. [n]--[next] ->to w next ->to w next

  3.  //代码1
     int tu[maxn][maxn];//图
     memset(tu,0,sizeof(tu));//初始化图 
     tu[a][b]=w;//添加一条a到b的边权为w的有向边
     tu[a][b]=tu[a][b]=w;//添加一条a到b的边权为w的无向边
     //遍历点x连的边
     for(int i=1;i<=n;i++)
         if(i!=x&&tu[x][i]!=0) { 
             printf("点x到点%d,边权为%d的边\n",i,tu[x][i]); 
         }
     
     //代码2
     int dis[n][n];
     int inf=99999999;
     for(int i=1;i<=n;i++)
         for(int j=1;j<=n;j++)
             if(i!=j)dis[i][j]=inf;
     cin>>u>>v>>w;
     dis[u][v]=w;
     
     
    
邻接表 存图
vector<int>v[maxn],g[maxn];//图 
for(int i=0;i<maxn;i++)  v[i].clear(),g[i].clear();//初始化图 

v[a].push_back(b);//添加一条a到b的边权为w的有向边
g[a].push_back(w); 

v[a].push_back(b); //添加一条a到b的边权为w的无向边
g[a].push_back(w);
v[b].push_back(a); 
g[b].push_back(w); 

//遍历点x的连的出边
for(int i=0;i<v[x].size();i++) {
    int to=v[x][i],w=g[x][i]; 
    printf("点x到点%d,边权为%d的边\n",to,w); 
}


采用vector
veotor<int>g[maxn];
struct node{
	int v;
    int w;
    node(int y,int z){
		v=y;
        w=z;
    }
};

加边
int x,y,z;
cin>>x>>y>>z;
g[c].push_back(node(y,z));

遍历
for(int i=0;i<g[u.id].size();i++){
    node y=g[u.id][i];
}

无环、任意两个点互通 (没有节点叫做空树,空树也是树)

1.孩子数组 (动态开点)
struct node{
	vector<int> childern,value;
}tree[N];
void add(int nod,int child,int w){
	tree[nod].childern.push_back(child);
    tree[nod].value.push_back(w);
}
void dfs(int now,int fa){
    for(int i=0;i<tree[now].child.size();i++){
		int next=tree[now].child[i];
        if(next!=fa){
			cout<<next<<" "<<tree[now].value[i];
        	dfs(next,now);
        }
    }
}

2.孩子数组 (固定开点)
#define lson (o<<1)
#define rson (o<<1|1)
#define father (o>>1)

int value[maxn];
int main(){
	int x=1;
    value[x]=5;
    value[lson]=6;
    value[rson]=7;
    cout<<father(lson(x))<<endl;
    return 0;
}
1.邻接矩阵存树
#define lson (o<<1)
#define rson (o<<1|1)
#define father (o>>1)

vector<int> v[maxn],g[maxn];
2.邻接表存树
3.链式前向星

使用一个数组存每一个结点的最后一条边,然后通过最后一条边找和这个点相连的其他边

4.树的遍历
  1. 前、中、后 序

11.排序

1.冒泡

o(n^2)

for(int i = 0;i < n-1 ;i++){
	for(int j = 0 ;j < n-i-1 ; j++){
        
	}
}

2.桶排序

(空间换时间)(0数据范围+m)

适合整数 且数据范围小的时候

# define Max 100005
int main(){
    int n;
    scanf("%d",&n);
    int a[Max];
    memset(a,0,sizeof(a));
    int date;
    
    for(int i=0;i<n;i++){//读入数据并装入桶中
        cin>>date;
        a[date]++;
    }
    
    for(int i=0;i<=n;i++){//按照桶来输出
        while(a[i]>0){
			cout<<i;
            a[i]--;
        }
    }
    printf("\n");
    return 0;
}

3.快速排序

//快速排序 函数
sort(a,a+n,cmp);

bool cmp(xuesheng a,xuesheng b){
   if(a.sum ==b.sum ) return a.k <b.k ;
   else return a.sum >b.sum ;
}

//底层模板
void QuickSort(ElementType A[],int low,int high){
    if(low<high){
        int pivotpos=Partition(A,low,high);
        QuickSort(A,low,pivotpos-1);
        QuickSort(A,pivotpos+1,high);
    }
}
int Partition(ElementType A[],int low,int high){
    ElementType pivot=A[low];
    while(low<high){
        while(low<high&&A[high]>=pivot)high--;
        A[low]=A[high];//比枢轴小的移动到左边
        while(low<high&&A[low]<=pivot)low++;
        A[high]=A[low];//比枢轴大的移动到右边
    }
    A[low]=povot;
    return low;//返回放枢轴的最终位置
}

4.归并排序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylY4Rzl6-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220306093152199.png)]

void merge(ElementType A[],int low,int mid,int high){
	for(int k=low;k<=high;k++){
		B[k]=A[k];//放在B中临时存放 
	}
	for(int i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
		if(B[i]<=B[j]){
			A[k]=B[i++];
		}else A[k]=B[j++];
	}
	while(i<=mid)A[k++]=B[i++];
	while(j<=high)A[k++]=B[j++];
} 
void MergeSort(ElementType A[],int low,int high){
	if(low<high){
		int mid=(low+high)/2;
		MergeSort(A,low,mid);
		MergeSort(A,mid+1,high);
		Merge(A,low,mid,high);
	}
}

12.递推递归

1.记忆化搜索-fib

搞一个数组把计算过值存起来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p2vzau9O-1663738769825)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220313090117956.png)]

13.简单数学与简约枚举

1.组合数

  1. 计算方法:C(n,m)=n!/(m!*(n-m)!)

  2. (性质1) C(n,k)=C(n,n-k)。(对称性)

  3. (性质2) C(n,k-1)+C(n,k)=C(n+1,k)。(递推公式)—左手三角形

  4. (性质3) ΣC(n,i)=C(n,0)+C(n,1)+…+C(n,n)=2^n。(横向求和)

  5. 打表 n<30 int n<64 long long

  6. C(n,0)=1,C(n,k+1)=C(n,k)*(n-k)/(k+1)。

long long c(int n , int m){
	long long ans =1;
    for (int i=o;i<m;i++){
		ans=ans*(n-1)/(n+1);
    }
    return ans ;
}
  1. 已知正整数N,可以将它拆分为若干正整数之和,问共有多少种不同的拆法?---------------挡板法

  2. 在1到N的范围内,有多少个正整数可以被2、3、5中的任意一个整除 ?------------容斥原理 能被2整除的数有N/2个,能被3整除的数有N/3个,能被5整除的数有N/5个

  3. 错排问题----D(n)=(n−1)[D(n−1)+D(n−2)]

2.枚举子集

1.增量构造法

int arr[10],pos[10];
//arr[]集合的数组
//pos[]选中子集下标的数组
//n 集合的元素个数
//cur 初始0 表示子集的个数
void print_subset0(int n,int*pos,int cur){
	//输出目前子集
    for(int i=0;i<cur;i++){
		printf("%d ",arr[pos[i]]);
    }
    printf("\n");
    //选择最小的未被选择的集合的一个元素
    int st=cur?pos[cur-1]+1:0;
    for(int i=st;i<n;i++){
        pos[cur]=i;
        print_subset0(n,pos,cur+1);
    }
}

14.位运算

按位与a&b两者同时为1则为1,否则为000101&11100=00100
按位或a l b有1为1,无1为000101 l 11100=11101
按位异或a^b相同为0,不同为100101^11100=11001
按位取反~a是1为0,是0为1~00101=11010
左移a<<b把对应的二进制数左移b位00101<<2=0010100
右移a>>b把对应的二进制数右移b位00101>>1=0010

一、取出x的第i位:y = ( x>> ( i-1 ) ) & 1

二、将x的第i位取反:x = x ^ ( 1<< ( i-1 ) )

三、将x的第i位变为1:x = x | ( 1<< ( i-1 ) )

四、将x的第i位变成0:x = x & ~( 1<< ( i-1 ) )

五、将x最靠右的1变成0:x = x & (x-1)

六、取出最靠右的1:y=x&(-x)

七、把最靠右的0变成1: x | = (x-1)

  1. n&(n-1):将n的二进制表示中最低位1改为0;

    例题:求数组中出现次数超过一半的元素?

    • 快速排序,排序后输出中间的那个数字。
    • 位运算统计每一位
  2. (a*b)%m==(a%m ×b%m)% m

  3. [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Z459MT3-1663738769825)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220410075103631.png)]

  4. image-20220410084852241
  5. image-20220410090034028

1.快速幂模板

long long Pow(long long  a,long long  b,long long  m){
	long long  ans =1;
    while(b>0){
		if(b&1){
			ans=ans*a%m;
        }
        a=a*a%m;
        b>>=1;
    }
    return ans;
}

2.快速乘模板

long long mul(long long  a,long long  b,long long  mod){
	long long  ans =0;
    a%=mod;
    while(b>0){
        ans+=a; 
        if(ans>mod) ans-=mod;
        a<<=1;
        if(a>=mod) a-=mod;
        b>>=1;
    }
    
    return ans;
}

15深度优先搜索

查找环

二分图检验(dfs染色)

for(int i=0;i<n;i++){
	if(col[i]==0&&flag){
        //图第一个颜色
        col[i]=1;
		dfs(i);
    }
}

void dfs(int now){
	if(!flag) return;
    
    for(int i=0;i<col[now].size();i++){\
        int next=col[now][i];
        if(!col[next]){
			col[next]=3-col[now];
            dfs(next);
  	 	 }
        else if(col[nest]==col[now]){
            flag=0;
            return;
        }
	}
    
}

16广度优先搜索

bfs搜图-队列

17.树状数组和线段树

离散化

去重

	for(int i=1;i<=n;i++){
		if(i==1||a[i]!=a[i-1])
			c[++tot]=a[i];
	}

树状数组(前缀和&单点修改)

下标从1开始

典型的区间更新,单点查询问题,用树状数组求解最方便

树状数组的查询前缀和

如果要求a序列[l,r]的区间和,只需要计算ask(r)-ask(l-1)即可。

int ask(int x){ 
	int ans=0; 
    while(x){ 
        ans+=c[x]; 
        x-=lowbit(x); 
    }
    return ans; 
}
树状数组的单点修改

要对序列a中的某个数a[x]进行修改,将其值增加y ,,对于维护的前缀和数组c我们需要进行相应的修改以确保区间和的正确性。

void insert(int x,int y){ 
	while(x<=N){ 
        c[x]+=y; 
        x+=lowbit(x); 
    } 
}
应用–求逆序对数
#include<iostream>  
#include<cstdio>  
#include<string.h> 
#include<algorithm> 
using namespace std;   
//求逆序对数 涉及离散化
const int N=5e5+5;
int a[N],b[N],c[N],t[N],tot;
//原数组 复制数组 离散化数组 树状数组 
int lowbit(int x){
	return x&(-x);
} 

int ask(int x){//
	int ans=0; 
    while(x){ 
        ans+=t[x]; 
        x-=lowbit(x); 
    }
    return ans; 
}


void insert(int x,int y){ 
	while(x<=tot){ 
        t[x]+=y; 
        x+=lowbit(x); 
    } 
}

int main()  
{   
    int n;
    while(scanf("%d",&n)&&n){
	 	
		for(int i=1;i<=n;i++){
	 		scanf("%d",&a[i]);
	 		b[i]=a[i];
		}
	
		tot=0;
		sort(a+1,a+1+n);
		memset(t,0,sizeof t);
		for(int i=1;i<=n;i++){//离散化 
			if(i==1||a[i]!=a[i-1])
				c[++tot]=a[i];
		}
	
		long long ans=0;
		for(int i=n;i>=1;i--){
			int idx=lower_bound(c+1,c+1+tot,b[i])-c;
			ans+=ask(idx); //查找前缀和 
			insert(idx,1); 
		}
		printf("%lld\n",ans);
	}
    return 0;  
}

线段树

下标从1开始

数组写法
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>

using namespace std;
#define ls rt<<1 //z*n
#define rs rt<<1|1 //z*n+1
const int N=5e4+5;
int a[N],t_max[N<<2],t_min[N<<2];//t数组维护区间信息 

void push(int rt){
	t_max[rt]=max(t_max[ls],t_max[rs]);//更新节点信息
	t_min[rt]=min(t_min[ls],t_min[rs]);
    //
}

void build(int rt,int l,int r){ 
	if(l==r){ 
		int x;
		scanf("%d",&x);
		t_max[rt]=x; 
		t_min[rt]=x; 
		return; 
	}
	int mid=l+r>>1; 
	build(ls,l,mid);//递归建树 
	build(rs,mid+1,r); 
	push(rt);//将信息由子节点更新到父节点 
}

int query_max(int rt,int l,int r,int L,int R){//[L,R]表示查询区间 
	if(L<=l&&r<=R){//1 
		return t_max[rt]; 
	}
	int mid=l+r>>1; 
	int ans=-0x3f3f3f3f;//负无穷 
	if(L<=mid)   ans=max(ans,query_max(ls,l,mid,L,R));//2 
	if(R>=mid+1)  ans=max(ans,query_max(rs,mid+1,r,L,R));//3 
	return ans; 
	
}


int query_min(int rt,int l,int r,int L,int R){//[L,R]表示查询区间 
	if(L<=l&&r<=R){//1 
		return t_min[rt]; 
	}
	int mid=l+r>>1; 
	int ans=1e8;
	if(L<=mid)   ans=min(ans,query_min(ls,l,mid,L,R));//2 
	if(R>=mid+1)  ans=min(ans,query_min(rs,mid+1,r,L,R));//3 
	return ans; 
	
}

//void insert(int rt,int l,int r,int x,int v){ 
//	if(l==r){ 
//		t[rt]=v; 
//		return ; 
//	}
//	int mid=l+r>>1; 
//	if(x<=mid){//修改的元节点包含在左节点 
//		insert(ls,l,mid,x,v); 
//	}else 
//	insert(rs,mid+1,r,x,v); 
//	push(rt);//更新信息 
//}

int main(){
	int n,m;
	while(scanf("%d%d",&n,&m)!=EOF){
		
//		for(int i=1;i<=n;i++){
//			cin>>a[i];
//		}
	
		memset(t_max,0,sizeof(t_max));
		memset(t_min,0,sizeof(t_min));
		build(1,1,n);
		while(m--){
			int L,R;
			scanf("%d%d",&L,&R);
			int ans=query_max(1,1,n,L,R)-query_min(1,1,n,L,R);
			printf("%d\n",ans);
		}
	
	
	}

	return 0;
}
结构体写法
#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1e5+10;
int n,m;
int a[maxn];
struct node
{
	int l,r;
	long long v;
	long long sum;
}tree[maxn*4];
void bulid(int x,int l,int r)
{
	tree[x].l=l;
	tree[x].r=r;
	if(tree[x].l==tree[x].r)
	{
		tree[x].sum=a[l];
		return;
	}
	int mid=(l+r)>>1;
	bulid(x<<1,l,mid);
	bulid(x<<1|1,mid+1,r);
	tree[x].sum=tree[x<<1].sum +tree[x<<1|1].sum ; 
}
void xp(int x)// 精髓 
{
	if(tree[x].v)
	{
		tree[x<<1].sum +=tree[x].v *(tree[x<<1].r -tree[x<<1].l +1);
		tree[x<<1|1].sum +=tree[x].v *(tree[x<<1|1].r -tree[x<<1|1].l +1);
		tree[x<<1].v +=tree[x].v ;
		tree[x<<1|1].v +=tree[x].v ;
		tree[x].v =0;
	}
}
void change(int x,int l,int r,int k)
{
	if(l<=tree[x].l&&tree[x].r<=r)
	{
		tree[x].sum +=k*(tree[x].r-tree[x].l +1);
		tree[x].v +=k;
		return ;
	}
	xp(x);
	int mid=(tree[x].l +tree[x].r )>>1;
	if(l<=mid)
	{
		change(x<<1,l,r,k);
	}
	if(r>mid)
	{
		change(x<<1|1,l,r,k);
	}
	tree[x].sum =tree[x<<1].sum +tree[x<<1|1].sum ;
}
long long query(int x,int l,int r)
{
	if(l<=tree[x].l &&tree[x].r <=r)
	{
		return tree[x].sum ;
	}
	xp(x);
	long long ans=0;
	int mid=(tree[x].l+tree[x].r )>>1;
	if(l<=mid)
	{
		ans+=query(x<<1,l,r);
	}
	if(r>mid)
	{
		ans+=query(x<<1|1,l,r);
	}
	return ans;
}

18.最短路径 wiki

Dijkstra

  1. n-1 n n
const int maxn = 105;
const int infinite = 1e9;
int edge[maxn][maxn]; //存边的权值
int vertex_num, edge_num;
int path[maxn];   //存放最短路径——>倒序存放——>若要打印原点到某点的距离,需要使用递归
int short_path_table[maxn]; //存放各点到原点最短路径的权值和
bool final[maxn];   //用于标记某点是否已求得到原点最短路径——>即已为确定名单

int v0=1;
void init()
{
    memset(final, 0, sizeof(final));
    memset(path, -1, sizeof(path)); //-1 用于标记此点还未加入
    short_path_table[v0] = 0;       //原点到原点的距离为0
    final[v0] = true;                  //标记 v0 已求得到原点最短路径
    for (int i = 1; i <= vertex_num; i++)
    { //导入初始时每个点到 v0 点的距离
        short_path_table[i] = edge[v0][i];
    }
}

void Dijkstra()
{
    init();
    //开始主循环,每次求得v0到某个v顶点的最短路径
    for (int m = 1; m < vertex_num; m++)
    { //共有 vertex_num - 1 个顶点需要找到最短路径
        int min = infinite;
        int min_index; //池子中最小值的下标
        //寻找离v0最近的顶点——>从short_path_table池子中选出最小值
        for (int i = 1; i <= vertex_num; i++)
        {
            if (!final[i] && short_path_table[i] < min)
            { //在final名单外选最小值
                min = short_path_table[i];
                min_index = i;
            }
        }
        final[min_index] = true; //将选中的最小值加入 final 名单
        //修正当前点周围点的最短路径及距离
        for (int i = 1; i <= vertex_num; i++)
        {
            // min + edge[min_index][i] < short_path_table[i]——>如果从 min_index 点到 i 点的距离比原来短时
            if (!final[i] && min + edge[min_index][i] < short_path_table[i])
            {
                short_path_table[i] = min + edge[min_index][i];//更新到 i 点最短长度
                path[i] = min_index;
            }
        }
    }
}

int main()
{	
	memset(edge,0x3f,sizeof(edge));//初始化 
    memset(short_path_table, 0x3f, sizeof(short_path_table));
	cin>>edge_num>>vertex_num;
	for(int i=1;i<=edge_num;i++){
		int a,b,c;
		cin>>a>>b>>c;
		if(edge[a][b]>c){//注意
			edge[a][b]=c;
			edge[b][a]=c;
		} 
	}
	Dijkstra();
	cout<<short_path_table[vertex_num]<<endl;

    return 0;
}

Bellman-Ford 算法 1

POJ-3259

const int max_edge = 5500;
const int max_vertex = 600;

struct EDGE
{
    int from, to, cost;
} edge[max_edge]; //存边——注意,这里只存了单向边

int dis[max_vertex]; //动态存储到_起点_的距离
int vertex_num, edge_num;

//求解从起点出发的最短距离,同时检查是否有“负圈”(注意:不同于“负边”)
// st 为起点
//若存在负圈则返回 false
bool shortest_path(int st)
{
    memset(dis, 0x3f, sizeof(dis)); //将初始值设为最大
    //***************************************************************
    //当将 dis 数组初始为 0 时负环之间将不会互相影响,于是可以独立找出所有的负环——>借助并查集
    //***************************************************************
    dis[st] = 0;                           //起点距离起点为 0
    for (int i = 0; i < vertex_num; i++)   //更新一次代表加入一个点
    {                                      //除去起点 v0,所以最多更新 vertex_num - 1 次
        for (int j = 0; j < edge_num; j++) //遍历所有的边
        {
            EDGE &e = edge[j]; //对 edge[j] 取一个别名
            if (dis[e.from] < 0x3f3f3f3f && dis[e.to] > dis[e.from] + e.cost)
            // dis[e.from] < 0x3f3f3f3f:避免使用未更新的点,发生溢出之类的异常
            { //若有优化——>相当于更新了一个点
                dis[e.to] = dis[e.from] + e.cost;
                if (i == vertex_num - 1)
                    //由于是从 0 开始,所以 vertex_num - 1 是第 vertex_num 次
                    return false;
            }
        }
    }
    return true; //检查完毕,所有的最短路径都在 vertex_num - 1 次中找到——>不存在负环
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {
        int path, worm;
        scanf("%d%d%d", &vertex_num, &path, &worm);
        edge_num = 2 * path + worm;
        for (int i = 0; i < 2 * path; i++)
        {
            scanf("%d%d%d", &edge[i].from, &edge[i].to, &edge[i].cost); //允许输入“重边”
            i++;
            edge[i].from = edge[i - 1].to;
            edge[i].to = edge[i - 1].from;
            edge[i].cost = edge[i - 1].cost;
        }
        for (int i = 2 * path; i < 2 * path + worm; i++)
        {
            scanf("%d%d%d", &edge[i].from, &edge[i].to, &edge[i].cost); //允许输入“重边”
            edge[i].cost = -edge[i].cost;
        }
        if (shortest_path(1)) //起点为 1
        {
            printf("NO\n");
        }
        else
        {
            printf("YES\n");
        }
    }
}

Floyd 算法

const int max_v = 1e4;
const int INF = 1e6; //注意 `溢出` 问题
int vertex_num;
int dis[max_v][max_v];  //邻接矩阵存图:从 i 到 j 的距离(不存在则为无穷大)
int path[max_v][max_v]; //从 i 到 j 最短路径的中第一步应该走的节点为 path[i][j]

void Floyd()
{
    //初始化记录路径的二维数组 path
    for (int st = 0; st < vertex_num; st++)
    {
        for (int ed = 0; ed < vertex_num; ed++)
        {
            path[st][ed] = ed; //从 st 到 ed 的第一步(当然也是最后一步)是 ed
        }
    }

    for (int transfer = 0; transfer < vertex_num; transfer++) //中转站0~transfer
        for (int st = 0; st < vertex_num; st++)               // st为起点
            for (int ed = 0; ed < vertex_num; ed++)           // ed为终点
            {
                // //避免溢出的写法
                // if (dis[st][transfer] < INF && dis[transfer][ed] < INF)
                // {
                //     //松弛操作
                //     dis[st][ed] = min(dis[st][ed], dis[st][transfer] + dis[transfer][ed]);
                // }
                //没有避免溢出的写法(可以通过将 INF 设置为一个比最短路最大值稍大的值来避免)
                if (dis[st][ed] > dis[st][transfer] + dis[transfer][ed])
                {
                    //松弛操作
                    dis[st][ed] = dis[st][transfer] + dis[transfer][ed];
                    //记录路径
                    path[st][ed] = path[st][transfer]; //先跟着到节点 transfer
                }
            }
}

//打印从 st 到 ed 的最短路径途径的节点
void print_path(int st, int ed)
{
    //从 st 出发
    printf("%d ", st);
    int current = path[st][ed]; //当前走到的节点
    while (current != ed)
    {
        printf("%d ", current);
        current = path[current][ed]; //下一步走的节点
    }
    printf("%d\n", ed); //最后到达 ed
}

19拓扑排序和强连通分量

拓扑排序

  1. 有向无环图才能拓扑排序
  2. 英⽂名叫 Directed Acyclic Graph,缩写是 DAG
  3. 入度为0 则入队。取出队首加入答案,入度减减
邻接表
vector<int>g[maxn];
priority_queue<int,vector<int>,greater<int > >q;

for(int i=1;i<=n;i++){//入度为0 ru
	if(in[i]==0){
		q.push(i);
	}
}
int ct=0;
while(!q.empty()){
	int f=q.front();
	q.pop() ;
	ans[++ct]=f;
	pos[f]=ct;
	for(int i=0;i<g[f].size() ;i++){ //删点去边 
		int v=g[f][i];
		in[v]--;
		if(in[v]==0){
			q.push(v);
		}
	}
}
if(ct<n) //说明有环
    
    
链式前向行版本
struct node{
    int to,next;
}e[maxn<<1];
void init(){
    vec.clear();
    cnt=tot=0;
    for(int i=1;i<=n;i++){
        pos[i]=deg[i]=head[i]=0;
    }
}
void add(int u,int v){
    e[++cnt].to=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}

//主函数部分
while(!que.empty()){    
    int x=que.front();
    que.pop();
    pos[x]=++tot;
    for(int i=head[x];i;i=e[i].next){
        deg[e[i].to]--;
        if(deg[e[i].to]==0){
            que.push(e[i].to);
        }
    }
}
if(tot!=n){//有环
    printf("NO\n");
}else{

强连通分量

  1. dfn[u] dfs到达n的次序号(时间戳)
  2. low[u] 从u出发的dfs树中最小的次序号
  3. dfs深搜(入栈、编号。遍历孩子,孩子没搜则搜则比较,搜则比较。入栈之后,dfn[x]==low[x]找到连通分量 出栈至t ==x
const int maxn=10005;
int num=0;
stack<int> s;
vector<int>g[maxn];
int num_lis=0;
int ins[maxn],dfn[maxn],low[maxn],tag[maxn];


void tarjan(int x){
	s.push(x);
    ins[x]=1;
    dfn[x]=low[x]=++num;
    for(int i=0;i<g[x].size() ;i++){
		int v=g[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else if(ins[v]) low[x]=min(low[x],dfn[v]);
    }
    if(dfn[x]==low[x]){
    	int t;num_lis++;
    	do{
    		t=s.top();
    		ins[t]=0;
    		tag[t]=num_lis;
    		s.pop();
		}while(t!=x);
	}
	return ;
}

20最小生成树

Kruskal 算法

  1. 处理边,排序后,利用并查集合并两个点
struct nod{
    int u,v,w;
}e[200005];//这里不是前向行的存边方法,只是正长的存边
bool cmp(nod a ,nod b){
    return a.w<b.w;
}
int fa[maxn];
int find(int x){
    if(fa[x]==x)return x;
    else return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
    int p = find(x);
    int q = find(y);
    if(p!=q){
        fa[p] = q;
        return true;
    }
    return false ;
}

//主函数部分
sort(e+1,e+m+1,cmp);//贪心的排序
long long  ans=0,cnt=0;


for(int i=0;i<m;i++){//n-1跳出
    if(cnt==n-1)break;
    else if(merge(e[i].u,e[i].v)){
        cnt++;
        ans+=e[i].w;

    }
}


for(int i=1;i<=m;i++){//n-1不跳出
   	 if(merge(e[i].u,e[i].v)){
       	if(cnt<n-1) ans+=e[i].w;
       	cnt++;
    }
}

Prime算法

  1. 处理点(两个集合搭桥)
	// prim 算法求最小生成树 
	
	#include <cstdio>
	#include <string>
	#include <cstring>
	#include <iostream>
	#include <algorithm>
	#define INF 0x3f3f3f3f
	using namespace std;
	const int maxn = 505;
	int a[maxn][maxn];
	int vis[maxn],dist[maxn];
	int n,m;
	int u,v,w;
	long long sum = 0;
	
	int prim(int pos) {
		dist[pos] = 0;
		// 一共有 n 个点,就需要 遍历 n 次,每次寻找一个权值最小的点,记录其下标
		for(int i = 1; i <= n; i ++) {
			int cur = -1;
			for(int j = 1; j <= n; j ++) {
				if(!vis[j] && (cur == -1 || dist[j] < dist[cur])) {
					cur = j;
				}
			}
			// 这里需要提前终止
			if(dist[cur] >= INF) return INF;
			sum += dist[cur];
			vis[cur] = 1;
			for(int k = 1; k <= n; k ++) {
			    // 只更新还没有找到的最小权值
				if(!vis[k]) dist[k] = min(dist[k],a[cur][k]);
			}
		}
		return sum;
	}
	
	int main(void) {
		scanf("%d%d",&n,&m);
		memset(a,0x3f,sizeof(a));
		memset(dist,0x3f,sizeof(dist));
		for(int i = 1; i <= m; i ++) {
			scanf("%d%d%d",&u,&v,&w);
			a[u][v] = min(a[u][v],w);
			a[v][u] = min(a[v][u],w);
		}
		int value = prim(1);
		if(value >= INF) puts("impossible");
		else printf("%lld\n",sum);
		return 0;
	} 






struct Node{
    int pos,val;
    bool operator < (const Node & a) const{
        return a.val<val;
    } 
};
priority_queue<Node> q;
int dis[105];
void Prime(int s){
    Node x,y;
    dis[s]=0;
    x.val=dis[s];x.pos=s;
    q.push(x);
    do{
        x=q.top();q.pop();
        int u=x.pos;
        sum+=dis[u];
        dis[u]=0;
        for(int i=1;i<=n;i++){
            if(Map[u][i]+dis[u]<dis[i]){
                dis[i]=Map[u][i]+dis[u];
                y.val=dis[i];
                y.pos=i;
                q.push(y);
            }
        }
    }while(!q.empty());
}

21二分图、二分图最大匹配

dfs染色

二分图:无向图、不含奇环图

//邻接表
vector<int>g[maxn];
bool dfs(int now,int col)
{
    color[now] = col;
    for(int i = 0;i<g[now].size();i++){
        int to = g[now][i];
        if(color[now] == color[to])return false;
        if(!color[to]){
            if(!dfs(to,3-col))return false;
        }
    }
    return true;
}

//链式前向星
bool dfs(int now,int col)
{
    color[now] = col;
    for(int i = head[now];~i;i = edge[i].next){
        int to = edge[i].to;
        if(color[now] == color[to])return false;
        if(!color[to]){
            if(!dfs(to,3-col))return false;
        }
    }
    return true;
}

匈牙利算法

  1. 交替路:交错路,从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
  2. 增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。
  3. 二分图中的一组匹配S是最大匹配,当且仅当图中不存在S增广路
  4. 如何找增广路? 匈牙利算法依次尝试给每一个左部节点x寻找一个匹配的右部节点y。右部点y能于左部点x匹配需要满足如下两个条件:y本身就是非匹配点,此时(x,y)就是非匹配边,自己构成长度为$ 1 $的增广路。y已经和左部一节点x′匹配,但是从x′出发能找到另一个右部点y′与之匹配。
  5. 本质桑使用的还是dfs递归的从x出发寻找增广路回溯时将路径上的匹配标记取反。
int vis[maxn];//当前有无找过
vector<int>g[maxn];
int match[maxn];//match【u】=v 成功
bool dfs(int x){//当前结点x 
    for(int i =0;i<g[x].size();i++){//相邻
        int v = g[x][i];
        if(!vis[v]){
            vis[v] = 1;
            if(!match[v]||dfs(match[v])){//没匹配 或者匹配的点有别人可以匹配
                match[v] = x;
                return true;
            }
        }
    }
    return false ;
}

//主函数内
int ans = 0;
memset(match,0,sizeof match);
for(int i = 1;i <= n;i++){
    memset(vis,0,sizeof(vis));
    if(dfs(i)){
        ans++;
    }
}

22[LCA ](LCA 最近公共祖先讲义.md)

Wiki笔记

朴素算法

向上标记法

基于二分搜索的算法(倍增)

//牛
int fa[maxn][31], dep[maxn];
void dfs(int u, int faa){
	fa[u][0] = faa, dep[u] = dep[faa] + 1;
	for(int i = 1; i <= 30; i++){//初始化
		fa[u][i] = fa[ fa[u][i - 1] ][i - 1];
	}
	for(int i = head[u]; i ; i = edge[i].next){ //遍历孩子 实现深搜
		int v = edge[i].v;
		if(v == faa)continue;
		dfs(v, u);
	}
} 

inline int lca(int x, int y){
	if(dep[x] < dep[y])swap(x,y);
	for(int i = 30; i >= 0; i--){ //大跳 跳至深度一致
		if(dep[ fa[x][i] ] >= dep[y]) x = fa[x][i];
	}
	if(x == y)return x;
	for(int i = 30; i >= 0; i--){  //大跳 一起跳
		if(fa[x][i] != fa[y][i]){
			x = fa[x][i], y = fa[y][i];
		}
	}
	return fa[x][0];
}

//朴素
const int max_v = 1e5 + 5; 
const int max_log_v = 20;  

vector<int> G[max_v];        
int root;                     
int parent[max_log_v][max_v]; 
int depth[max_v],in[max_v];             
int vertex_num;               

void dfs(int v,int p,int d ){
	parent[0][v]=p;
	depth[v]=d;
	for(int i=0;i<G[v].size() ;i++){//注意< 不是<= //是V 
		int to=G[v][i];
		if(to!=p){//注意 父节点
			dfs(to,v,d+1);
		}
	} 
}

void init(){
	dfs(root,-1,0);
	for(int k=0;k+1<max_log_v;k++){
		for(int i=1;i<=vertex_num;i++){
			if(parent[k][i]==-1) parent[k+1][i]=-1;
			else {
				parent[k+1][i]=parent[k][parent[k][i]];
			} 
		}
	}
} 

int lca(int u,int v){
	if(depth[u]>depth[v]){
		swap(u,v);
	}
	//跳到相同高度
	for(int k=0;k<max_log_v;k++){
		if((depth[v] - depth[u]) >> k & 1){
			v=parent[k][v];
		}
	} 
	//特判 
	if(u==v) return u;
	//一起向上跳
	for(int k=max_log_v-1;k>=0;k--){//注意-1 
		if(parent[k][u]!=parent[k][v]){
			u=parent[k][u];
			v=parent[k][v];
		}
	}	
	return parent[0][u];
}

int main()
{
    int t;
    cin>>t;
    while(t--){
    	scanf("%d", &vertex_num);
    	//初始化标记数组 
    	cla(in);
    	//clb(depth);
    	for(int i=0;i<max_v;i++){
    		G[i].clear() ;
		}
    	//输入 
    	for (int i = 1; i < vertex_num; i++) 
    	{
        	int p, b;
        	scanf("%d%d", &p, &b);
        	G[p].push_back(b);
        	G[b].push_back(p);
        	in[b]++;
    	}
    	int x, y;
    	scanf("%d%d", &x, &y);
    	
    	//找到根节点 
    	for(int i=1;i<=vertex_num;i++){
    		if(in[i]==0){
    			root=i ;
				break;   			
			} 
		} 
		//cout<<"root "<<root<<endl;
        
    	//lca 
   		init();
    	printf("%d\n", lca(x, y));
    
	}
    return 0;
}


const int max_v = 5e5 + 5; //节点数
const int max_log_v = 20;  //最多向上走 2^max_log_v 次,可以稍微开大一点(如果节点数增多,此数也会变化)

// vertex 编号从 0 开始

vector<int> G[max_v];         //图的邻接表表示——>存储各节点的孩子节点
int root;                     //根节点编号
int parent[max_log_v][max_v]; //向上走 2^k 步所到的节点(超过根时记作-1)
int depth[max_v];             //存储节点深度
int vertex_num;               //存储节点数

//前序遍历
//预处理出 parent[0][x],并标记深度——>为之后 parent 的动态规划做铺垫
void dfs(int v, int p, int d) // vertex & parent & depth
{
    parent[0][v] = p; // 2^0 = 1
    depth[v] = d;
    for (int i = 0; i < G[v].size(); i++)
    {
        if (G[v][i] != p) //避免指回父节点
            dfs(G[v][i], v, d + 1);
    }
}

//预处理——>降低查询**多次查询**时的 LCA 的复杂度
void init()
{
    dfs(root, -1, 0); //预处理出 parent[0][x] 和 depth
                      //注意,这里将根节点的父节点特殊设为 -1

    //通过**动态规划**预处理出 parent——>先小步,再大步
    for (int k = 0; k + 1 < max_log_v; k++) // k + 1 < max_log_v——>避免超出数组长度
    {
        for (int i = 1; i <= vertex_num; i++)
        {
            if (parent[k][i] == -1) //如果已经超过根节点,则更高次幂也会超过根节点
                parent[k + 1][i] = -1;
            else
                parent[k + 1][i] = parent[k][parent[k][i]]; //倍增——>爸爸的爸爸是爷爷
        }
    }
}

//计算 u & v 两点的 LCA
int lca(int u, int v)
{
    if (depth[u] > depth[v]) //使得 v 深度 大于等于 u
        swap(u, v);
    for (int k = 0; k < max_log_v; k++)
    {
        if ((depth[v] - depth[u]) >> k & 1) //将两点的深度差值从低到高取出,进行跳跃——>先小跳再大跳
        {                                   //由于每一位都以二进制形式取出了,最后的值刚好够两点深度相等
            v = parent[k][v];
        }
    }
    if (u == v) //特判当 (u, v) 的公共祖先为 u 时——>此时则不需要继续再跳了
        return u;
    for (int k = max_log_v - 1; k >= 0; k--)
    {                                     //二分法思想——>先大后小的进行跳跃
        if (parent[k][u] != parent[k][v]) //排除了跳过头时(包括 -1 == -1)
        {
            u = parent[k][u];
            v = parent[k][v];
        }
    }
    return parent[0][u]; //返回向上再走一步的的节点
}

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out_mine.txt", "w", stdout);
    int query_num;
    scanf("%d%d%d", &vertex_num, &query_num, &root);
    for (int i = 1; i < vertex_num; i++) //只有vertex-1条边
    {
        int p, b;
        scanf("%d%d", &p, &b);
        G[p].push_back(b);
        G[b].push_back(p);
    }
    init(); // LCA 倍增法初始化

    for (int i = 0; i < query_num; i++)
    {
        int x, y;
        scanf("%d%d", &x, &y);
        printf("%d\n", lca(x, y));
    }

    return 0;
}

Tarjan 算法

const int maxn = 5e5 + 10; //最大节点数
vector<int> G[maxn];       //存树
int fa[maxn];              //并查集的父节点
int vertex_num;
int query_num;
int root;       //节点编号
int ans[maxn];  //第 i 个答案为 ans[i]
bool vis[maxn]; //我们要对树进行前序遍历 & 后序遍历
//如果在前序遍历中节点 i 已经被访问过了,则 vis[i] = true
struct QUERY
{
    int another; //被询问的另一个节点
    int q_id;    //在第 q_id 次被问
};
vector<QUERY> query[maxn]; //第 i 个节点上被询问其和另一个节点的最近公共祖先

void init(int n) //并查集初始化(n 个)
{
    for (int i = 0; i <= n; i++)
    {
        fa[i] = i; //指向其本身
    }
}

// 找到在集合中节点 x 的代表节点
int get_representative(int x) //递归思想
{
    if (fa[x] == x)
        return x;
    return fa[x] = get_representative(fa[x]); //在查询根节点的过程中进行路径压缩
}

//利用 Tarjan 算法对之前的问题进行离线处理
//并将第 i 个答案存在 ans[i] 中
void Tarjan(int current, int p)
{
    vis[current] = true; //标记前序遍历中此节点已经被访问过了——>在后序遍历时则可以直接回答询问
    for (auto to : G[current])
    {
        if (to != p)
        {
            Tarjan(to, current); //递归
            fa[to] = current;    //递归完毕后将子节点归并到父节点 current 中
        }
    }

    //在后序遍历中尝试回答 query
    for (auto q : query[current])
    {
        //当前查询的为 [current, q.another] 的 `最近公共祖先`
        if (vis[q.another]) //如果另外一个节点已经在前序遍历中访问过了,则可以回答此询问
        {
            ans[q.q_id] = get_representative(q.another); //存储答案到 ans 中
        }
    }
}

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out_mine.txt", "w", stdout);
    scanf("%d%d%d", &vertex_num, &query_num, &root);
    for (int i = 1; i < vertex_num; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        G[a].push_back(b);
        G[b].push_back(a);
    }

    for (int i = 0; i < query_num; i++)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        query[a].push_back(QUERY{b, i}); //询问的另一个节点 & 询问编号
        query[b].push_back(QUERY{a, i});
    }

    init(vertex_num); //并查集初始化
    Tarjan(root, -1); //从 root 开始 Tarjan 离线回答询问;-1: 根节点没有父节点

    for (int i = 0; i < query_num; i++)
    {
        printf("%d\n", ans[i]); //按之前离线处理的顺序回答询问
    }
}

基于 RMQ 的算法

const int max_v = 5e5 + 10;
const int max_log_v = 100; // 2^20 足够囊括所有的点

vector<int> G[max_v]; //图的邻接表表示
int root;             //有根树的`根节点`
int vertex_num;       //树中节点的个数

//保存第 i 次 DFS 中 前序 & 后序遍历 访问到的节点信息
struct ORDER
{
    int vertex_idx;     //节点编号
    int depth;          //深度
} dfs_order[max_v * 2]; //由于 前序 & 后序遍历都要记录,所以要 *2

int vis_order[max_v]; //节点 i 第一次被 DFS 访问(前序遍历)是在 vis_order[i] 次

//预处理出的 `稀疏表`,f[k][i] 表示在`访问顺序`区间 [i, i+(1<<k)-1] 中 `depth` 的最小值 及其编号
pair<int, int> f[max_log_v][max_v * 2]; //其中 first 代表其区间最大值,second 表示其对应的节点编号

// DFS 前序 & 后序遍历,并记录节点的信息到 dfs_order 中
void dfs(int v, int parent, int d, int &k)
{ // vertex & parent & depth & k 用来记录访问顺序
    vis_order[v] = ++k;
    dfs_order[k] = ORDER{v, d}; //记录前序遍历
    for (int i = 0; i < G[v].size(); i++)
    {
        const int &to = G[v][i];
        if (to != parent)
        {
            dfs(to, v, d + 1, k);
            dfs_order[++k] = ORDER{v, d}; //记录后序遍历
        }
    }
}

//`稀疏表`预处理
// len: 需要处理的长度
//这里是在维护 节点的访问顺序,使其后续能够在 O(1) 的时间内找出
//从第 st 到第 ed 个被访问的节点中深度最 `小` 的节点 `编号`
void sparse_table_init(int len)
{
    //先导入节点数据到 f[0][i]
    for (int i = 1; i <= len; i++)
    {
        f[0][i] = make_pair(dfs_order[i].depth, dfs_order[i].vertex_idx);
        //第 i 个被访问的节点的深度 & 编号
        // printf("f[0][%d]: %d %d\n", i, dfs_order[i].depth, dfs_order[i].vertex_idx);
    }

    // idx 代表当前的指数,从 1 (2^1 = 2) 开始
    int logn = 31 - __builtin_clz(len);
    for (int idx = 1; idx <= logn; idx++)
        for (int pos = 1; pos + (1 << idx) - 1 <= len; pos++)
            // pos 代表当前处理到的位置
            f[idx][pos] = min(f[idx - 1][pos],
                              f[idx - 1][pos + (1 << (idx - 1))]); //递推(原理类似于 LCA 倍增)
}

//查询从 st 到 ed 中的 深度 `depth` 最小值对应的`下标`
inline int query_min_idx(int st, int ed)
{
    int lg = 31 - __builtin_clz(ed - st + 1); //求出 st 到 ed 长度的二进制指数
    //两个重合区间构成了整个 [st, ed] 区间,此时只需要返回这两个区间的最大值即可

    if (f[lg][st] < f[lg][ed - (1 << lg) + 1])
        return f[lg][st].second; //如果在左边区间深度较小
    else                         //否则在右边区间深度较小,并返回节点的编号
        return f[lg][ed - (1 << lg) + 1].second;
}

//预处理出 vs、depth 和 vis_order
void init(int v)
{
    int k = 0;
    dfs(root, -1, 1, k);
    //预处理出 RMQ (返回的不是最小值,而是最小值对应的下标)
    sparse_table_init(2 * v - 1); //前序遍历途径 v 个节点,后续遍历途径 v-1 个节点(每一条边都会导致后续遍历一次)
}

//计算 u 和 v 的 LCA
int lca(int u, int v)
{ // min 和 max 保证了传入的参赛 st <= ed
    return query_min_idx(min(vis_order[u], vis_order[v]), max(vis_order[u], vis_order[v]));
}

template <typename T>
inline T read() // 声明 template 类,要求提供输入的类型T,并以此类型定义内联函数 read()
{
    T sum = 0, fl = 1;  // 将 sum,fl 和 ch 以输入的类型定义
    int ch = getchar(); // fl 标记是否是负数
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            fl = -1;
    for (; isdigit(ch); ch = getchar())
        sum = sum * 10 + ch - '0';
    return sum * fl;
}

int main()
{
    int query_num;
    // scanf("%d%d%d", &vertex_num, &query_num, &root);
    vertex_num = read<int>();
    query_num = read<int>();
    root = read<int>();

    for (int i = 1; i < vertex_num; i++) //只有vertex-1条边
    {
        int p, b;
        // scanf("%d%d", &p, &b);
        p = read<int>();
        b = read<int>();
        G[p].push_back(b);
        G[b].push_back(p);
    }

    init(vertex_num); //初始化

    //求 x,y 的最近公共祖先
    for (int i = 0, x, y; i < query_num; i++)
    {
        // scanf("%d%d", &x, &y);
        x = read<int>();
        y = read<int>();
        printf("%d\n", lca(x, y));
    }

    return 0;
}

23树上差分

wiki

如果有一个区间内的权值发生相同的改变的时候,我们可以采用差分的思想方法

按点差分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkdpZZ5P-1663738769826)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220820075428311.png)]

按边差分

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aEJv3Aa-1663738769826)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220820075437985.png)]

24状态压缩DP、区间DP

状态压缩+动态规划 。既然是dp那么最为重要的就是找到状态转移方程然后转移就行。不同的在于状压dp利用二进制把状态记录成二进制数。

互不侵犯(状压DP)

互不侵犯

f[i][j][s]=sum(f[i−1] [k] [sgs[j]]),f[i] [j] [s]就表示在只考虑前i行时,在前i行(包括第i行)有且仅有s个国王,且第i行国王的情况是编号为j的状态时情况的总数。而k就代表第i-1行的国王情况的状态编号

#include<bits/stdc++.h>
using namespace std;
long long maze[10][1<<10][100];//记得要开long long 
int f[1<<10],num[1<<10];
int check(int a,int b)
{//判断上下两行是否合法
    if((a<<1)&b) return 0;
    if((a>>1)&b) return 0;
    if(a&b) return 0;
    return 1;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    int siz=1<<n;
    for(int i=0;i<siz;i++)
    {//预处理一行里有哪些状态是可行的
        if(!(i&(i<<1))&&!(i&(i>>1)))
        {
            f[i]=1;
            int tmp=i;
            while(tmp)//预处理每种状态的国王数量
            {
                if(tmp&1) num[i]++;
                tmp=tmp>>1;
            }
        }
    }
    for(int i=0;i<siz;i++)//由于第一行上面没有格子,所以需要单独处理
        if(f[i]) maze[1][i][num[i]]=1;
    
    for(int i=2;i<=n;i++)
    {//枚举第几行
        for(int j=0;j<siz;j++)//枚举这一行的状态
        {
            if(!f[j]) continue;
            for(int k=0;k<siz;k++)//枚举上一行的状态
            {
                if(!check(k,j)||!f[k]) continue;
                for(int l=m;l>=num[j];l--)
                {
                    maze[i][j][l]+=maze[i-1][k][l-num[j]];
                }
            }
        }
    }
    long long ans=0;
    for(int i=0;i<siz;i++)
        ans+=maze[n][i][m];
    printf("%lld\n",ans);
    system("pause");
    return 0;
}

石子合并(区间DP)

区间dp就是利用了分治的思想,将整个区间不断的拆分一下,将一个区间[l,r]分成[l,k] [k+1,r],然后再对[l,k]和[k+1,r]进行类似的拆分,直到拆分成最小的区间

合并石子

不错的题解

#include <iostream>
#include <cstring>
using namespace std;
const int N=310;
int n;
int a[N];
int s[N];
int f[N][N];
int int main(int argc, char const *argv[])
{
	memset(f,0x3f,sizeof(f));
	cin>>n;
	for (int i = 1; i <=n; ++i)
	{
		cin>>a[i];//每堆石子的质量
		s[i] = s[i-1]+a[i];//求前缀和
		f[i][i] = 0;//合并每一堆石子的代价为0
	}
	for (int len = 2; len<=n; len++)
	{
		for (int l=1; l+len-1 <=n; ++l)
		{
			int r = l+len-1;
			for (int k=l; k<r ; k++)
			{
				f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[r-1]);
			}
		}
	}
	cout<<f[1][n]<<endl;
	return 0;
}#include<iostream>  
#include<cstdio>  
#include<cmath> 
#include <cstring> 
const int INF=999999999;
using namespace std;   
int n,minl,maxl,f1[300][300],f2[300][300],num[300];  
int s[300];  
int main()  
{   
	memset(f2,0x3f,sizeof(f2));
    scanf("%d",&n);  
    for(int i=1;i<=n+n;i++)
    {  
        scanf("%d",&num[i]);  
        num[i+n]=num[i];  
        s[i]=s[i-1]+num[i]; 
		f2[i][i]=0;
    }  
    for(int p=1;p<n;p++)  
    {  
        for(int i=1,j=i+p;(j<n+n) && (i<n+n);i++,j=i+p)  
        {   
            for(int k=i;k<j;k++)  
            {  
                f1[i][j] = max(f1[i][j], f1[i][k]+f1[k+1][j]+s[j]-s[i-1]);   
                f2[i][j] = min(f2[i][j], f2[i][k]+f2[k+1][j]+s[j]-s[i-1]);  
            }  
        }  
    }  
    minl=INF;  
    for(int i=1;i<=n;i++)  
    {  
        maxl=max(maxl,f1[i][i+n-1]);  
        minl=min(minl,f2[i][i+n-1]);  
    }  
    printf("%d\n%d",minl,maxl);  
    return 0;  
}

25数论

wiki

矩阵

1、求解线性方程组
const int eps=1e-8;
typedef vector<double>vec;
typedef vector<vec>mat;//定义矩阵
vec gauss_jordan(const mat& A,const vec& b){//有解返回一个向量即x,无解或无穷多解返回长度为0的向量;
    int n=A.size();
    mat B(n,vec(n+1));
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)B[i][j]=A[i][j];
    for(int i=0;i<n;i++)B[i][n]=b[i];
    for(int i=0;i<n;i++){
        int pivot=i;
        for(int j=i;j<n;j++)
            if(abs(B[i][j])>abs(B[pivot][i]))pivot=j;
        swap(B[i],B[pivot]);
        if(abs(B[i][i])<eps)return vec();//返回长度为0的向量vec
        for(int j=i+1;j<=n;j++)B[i][j]/=B[i][i];
        for(int j=0;j<n;j++){
            if(i!=j){
                for(int k=i+1;k<=n;k++)B[j][k]-=B[j][i]*B[i][k];
            }
        }
    }
    vec x(n);
    for(int i=0;i<n;i++)x[i]=B[i][n];
    return x;
}
2、快速幂与矩阵快速幂
快速幂:

解决 ab % c 计算过程爆longlong的问题

typedef long long ll;
ll qpow(ll a,ll b,ll mod){
	ll ans=1;
    while(b>0){
		if(b&1)ans=(ans*a)%mod;
        a=a*a%mod;
        b>>=1;
    }
    return ans;
}
矩阵快速幂:

计算An,应用:通过把数放到矩阵的不同位置,然后把普通递推式变成"矩阵的等比数列",最后快速幂求解递推式

struct node {
	int mat[15][15];//定义矩阵 
}x,y; 
node mul(node x,node y){//矩阵乘法 
	node tmp;
	for(int i=0;i<len;i++){
		for(int j=0;j<len;j++){
			tmp.mat [i][j]=0;
			for(int k=0;k<len;k++){
				tmp.mat [i][j]+=(x.mat [i][k]*y.mat [k][j])%mod;
			}
			tmp.mat [i][j]=tmp.mat[i][j]%mod;
		}
	}
	return tmp;
} 
node matpow(node x,node y,int num){//矩阵快速幂 ,y为答案矩阵 num为幂
	while(num){
		if(num&1){
			y=mul(y,x);
		}
		x=mul(x,x);
		num=num>>1;
	}
	return y;
} 

质数

试除法判断质数
int isprime(int n)
{	
	if(n == 1) return 0; 
	if(n == 2 || n == 3) return 1;
    for (int i = 2; i*i <= n; i++)
        if (n%i == 0)
            return 0;
    return 1;
}

埃式筛法打质数表

基本思想:初始将所有大于等于 2 的数放在一个集合中,每次筛选后集合中剩余最小的数是质数,将它的倍数去掉。

算法结束时,没有被筛去的数就是质数。每个数要被自己所有的因子标记一遍,所以普通筛的时间复杂度为O(nloglogn)

bool isprime[maxn];// 1 素数  0 和数
void sieve(int n){
	for(int i=1;i<=n;i++) isprime[i]=1;
    isprime[0]=isprime[1]=0;
    for(int i=2;i*i<=n;i++){ //2-> 根号n
        if(isprime[i]){
			for(int j=2*i;j<=n;j+=i) isprime[j]=0;
        }
    }
}
//返回1~n-1中有多少个质数
int sieve(int n){
    int num=0;//
    for(int i=0;i<n;i++)isprime[i]=1;
    isprime[0]=isprime[1]=0;
    for(int i=2;i<n;i++){
		if(isprime[i]){
            num++;//加一点
            for(int j=2*i;j<n;j+=i)isprime[j]=0;
        }
    }
    return num;//
}

#include<iostream>
#include<stdio.h>
#include<cstring>
using namespace std;
const int maxn = 1e8;
bool isPrime[maxn + 5];
int cnt=0;

int main(){
	long long n;
	scanf("%lld",&n);
    isPrime[1] = 1; 
    for(int i = 2; i*i <= n; i++)
        if(isPrime[i]==0){
            for(int j = i+i;  j <= n; j+=i)
                isPrime[j] = 1;
	}
	for(int i=1;i<=n;i++)
	{
		if(isPrime[i]==0) 
			cnt++;
	}	
	printf("%lld\n",cnt);
	return 0;
}
质数线性筛–欧拉筛

每个合数必为它的最小质因子和一个**最大因数(除它本身)**的乘积

const int maxn=120005;
int prime[maxn];//1~n存找到的素数
int num;//记录当前为止找到的素数个数
int vis[maxn];//是否访问过该数
void euler(int n){
    memset(prime,0,sizeof(prime));
	memset(vis,false,sizeof(vis));
    scanf("%d",&n);
    vis[1]=true; 
    for(int i=2;i<=n;i++){  //每个i
    	if(!vis[i]){
    		vis[i]=true;
    		prime[++num]=i;
		}
	    for(int j=1;j<=num&&i*prime[j]<=n;j++){ //质数的i倍都删除
			vis[prime[j]*i]=true;//筛除 
			if(i%prime[j]==0) break;
		} 
	}
}
质数区间筛
//用于求出一段区间[a,b)内的全部质数
//O(Mlog(logn))m为区间长度,n为最大的r
//为避免a、b过大,可以用i-a来计入数组
bool isprime[maxm];//a->b
bool isprime0[maxm];//2->sqrt(b)
ll prime[maxn];//记录素数的值,方便调用
void segment_sieve(ll a,ll b){
    //init:对[2,sqrt(b))内全部初始化为质数
    for(ll i=0;i*i<b;i++)isprime0[i]=1;
    //init:isprime[i-a]=1;
    for(ll i=0;i<b-a;i++)isprime[i]=1;
    for(ll i=2;i*i<b;i++){
        if(isprime0[i]!=0){
			for(ll j=2*i;j*j<b;j+=i)isprime0[j]=0;//筛选[2,sqrt(b));
            //(a+i-1)/i得到最接近a的i的倍数,最低是i的2倍,然后筛选,两者取max
            for(ll j=max(2LL,(a+i-1)/i)*i;j<b;j+=i)isprime[j-a]=0;
        }
    }
    int num=0;
    for(ll i=0;i<b-a;i++){
        if(isprime[i])prime[num++]=i+a;
    }
    return;
}
  
int main()
{
  ll a,b;
  cin>>a>>b;
  segment_sieve(a,b);
  for(ll i=0;i<b-a;i++)     
  	if(is_prime[i])
      ans++; 
  cout<<ans<<endl; 
  return 0;
}

模运算

1.最大公约数gcd

要注意负数要转成正数再算 gcd ⁡

lcm=|a*b|/gcd(a,b)

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

裴蜀定理exgcd

要注意负数要转成正数再算 gcd ⁡ \gcdgcd

又称贝祖定理,任意a,b不全为0的整数,存在整数x,y,使得:

a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)

可以将朴素欧几里得算法回带带出x,y证明 其正确性。

ll exgcd(ll a,ll b,ll &x,ll &y){
    if(a==0&&b==0)return -1;
    if(b==0){
        x=1;y=0;
        return a;
    }
    ll ans=exgcd(b,a%b,y,x);
    y-=a/b+x; //注意这里做了修改  *->+
    return ans;
}
2、求逆元

逆元定义

a*y %p ≡ 1%p,则y为a的逆元。//解决除法

求逆元

a y = 1 + m k ay=1+mk ay=1+mk a y − m k = 1 ay-mk=1 aymk=1 求y与k

此为ax+by=gcd(a,b) 模型,可以使用exgcd来解

int exgcd(int a,int b,int &x,int &y){
    if(a==0&&b==0)return -1;//无解
    if(b==0)x=1,y=0,return;
    int ans=exgcd(b,a%b,y,x);
    y-=a/b+x;
    return ans;
}
//跑完mod_inverse后返回a的逆元,x中存"y"值,y中存"-k"的值
int mod_inverse(int a,int m){
    int x,y;
    exgcd(a,m,x,y);
    return (m+x%m)%m;
}
//求在 mod=p 下 1~n 的逆元(打表)
void inv_arrey(int n, int p){
    inv[1] = 1;
    for (int i = 2; i <= n; ++i){
        inv[i] = (long long)(p - p / i) * inv[p % i] % p;
    }
}
3、费马小定理

当p为素数时 ∀ x , x p ≡ x ( m o d p ) ∀x,x^p≡x(mod p) x,xpx(modp)

∴ ∀无法被p整除的x, x ( p − 1 ) ≡ 1 ( m o d p ) x^{(p-1)}≡1(mod p) x(p1)1(modp)

x ( p − 2 ) ≡ x − 1 ( m o d p ) x^{(p-2)}≡x^{-1}(mod p) x(p2)x1(modp) 可以用此公式来计算逆元

即:
a − 1 ≡ a p − 2 ( m o d p ) a^{-1}≡a^{p-2}(mod p) a1ap2(modp)

//费马小定理求逆元
//快速幂
ll binaryPow(ll base, ll expo, ll p){
    if (expo == 0) return 1;
    if (expo % 2 == 1)
        return base * binaryPow(base, expo-1, p) % p;
    else{
        ll mul = binaryPow(base, expo/2, p) % p;
        return mul % p * mul % p;
    }
}
//在(mod p)的意义下,b的乘法逆元
ll inv(ll b) {return binaryPow(b, mod-2, mod);}
4、欧拉定理与欧拉函数

费马小定理中的mod数p必须为素数,推广后在欧拉定理与欧拉函数中,p可以是任意实数。

欧拉函数

欧拉函数的值表示[0,m-1]区间内与m互素的数的个数,规定φ(1)=1,而当m本身为素数时,φ(m)=m-1;

//给出m求φ(m) O(m^0.5)
int euler_phi(int m){
    int res=m;
    for(int i=2;i*i<=m;i++){
        if(m%i==0){
            res=res/i*(i-1);
            for(;m%i==0;m/=i);
        }
    }
    if(m!=1)res=res/m*(m-1);
    return res;
}
//打表1~n的欧拉值,利用埃式筛
int euer[maxn];
void euler_phi(){
    for(int i=0;i<maxn;i++)euler[i]=i;
    for(int i=2;i<maxn;i++){
        if(euler[i]==i){
            for(int j=i;j<maxn;j+=i)euler[j]=euler[j]/i*(i-1);
        }
    }
    return;
}
欧拉函数的性质:

Ⅰ.当p为素数,k>=1时有
φ ( p k ) = p k − p k − 1 = p k − 1 ( p − 1 ) = p k ( 1 − 1 / p ) φ(p^k)=p^k-p^{k-1}=p^{k-1}(p-1)=p^k(1-1/p) φ(pk)=pkpk1=pk1(p1)=pk(11/p)
Ⅱ.欧拉函数为积性函数,如果m和n互素,则
φ ( m n ) = φ ( m ) φ ( n ) φ(mn)=φ(m)φ(n) φ(mn)=φ(m)φ(n)

欧拉定理

若m是正整数,gcd(k,m)=1,则
k φ ( m ) ≡ 1 ( m o d m ) k^{φ(m)}≡1(mod m) kφ(m)1(modm)

5、组合数
//阶乘+快速幂求逆元=求组合数
ll fac[maxn];//预处理阶乘表
void serFac(int n){
    fac[0]=1;
    for(int i=1;i<=n;++i){
        fac[i]=1LL*fac[i-1]*i%mod;
    }
}
ll binaryPow(ll a,ll b,ll m){
    ll ans=1;
    while(b){
        if(b&1)ans=ans*a%m;
        a=a*a%m;
        b>>=1;
    }
    return ans;
}
ll C(int n,int m){
    if(n<m)return 0;
    if(n<0||m<0)return 0;
    ll t=fac[n-m]*fac[m]%mod;
    ll inv=binaryPow(t,mod-2,mod);
    return fac[n]*inv%mod;
}

解同余方程

a x 0 ≡ b ( m o d m ) ax0\equiv b(mod m) ax0b(modm)

//解同余方程$ax0\equiv b(mod m)$中最小的x0,若无解返回false
bool ModularEqu(int a,int b,int m,int &x0){
    int x,y,k;
    int d=exgcd(a,m,x,y);
    if(b%d==0){
        x0=x*(b/d)%m;k=m/d;x0=(x0%k+k)%k;
        return true;
    }
    return false;
}

26trie,kmp,字符哈希

字典树

规模取决于存储单词的数量单词间的差异

匹配

#include<iostream>
#include<map>
#include<string.h>
#include<stdio.h>
using namespace std;
int main(){
	map<string, int>s;
	char a[20];
	while(gets(a) && a[0] != '\0'){
		for(int i = strlen(a);i>0;i--){
			a[i]='\0';
			s[a]++;		
		}
	}
	while(gets(a)){
		cout<<s[a]<<endl;
	}
	return 0;
} 

KMP


char A[1000006],B[1000006];//原串 模式串
int next[1000006]; 
void KMP(){
	int la=strlen(A+1);
	int lb=strlen(B+1);
	int i,j;
    //求出next数组
    //b串自己与自己匹配 前缀为模式串 后缀为主串
    //j是b串前缀的指针
	next[1]=j=0;
	for(i=2;i<=lb;i++){
		while(j>0&&B[j+1]!=B[i]) j=next[j];
		if(B[j+1]==B[i]) next[i]=++j;//匹配
	}
	j=0;
	for(i=1;i<=la;i++){//进行匹配
		while(j>0&&B[j+1]!=A[i]) j=next[j];//回退 j==0便忽略 然后增加i
		if(B[j+1]==A[i])j++;//单个字符匹配成功
		if(j==lb) {//全部匹配成功
			printf("%d\n",i-lb+1);
			j=next[j];
		} 
	}
}

//https://vjudge.net/contest/512179#problem/B
#include<bits/stdc++.h>
using namespace std;
int n,k,len1,len2,t,ans=0,ans1[1000005];
int next[1000005];
char A[1000005],B[1000005];

void KMP(){
	int la=strlen(A+1);
	int lb=strlen(B+1);
	int i,j;
	next[1]=j=0;
	for(i=2;i<=lb;i++){//求出next数组
		while(j>0&&B[j+1]!=B[i]) j=next[j];
		if(B[j+1]==B[i])j++;
		next[i]=j;
	}
	j=0;
	for(i=1;i<=la;i++){//进行匹配
		while(j>0&&B[j+1]!=A[i]) j=next[j];//回退 
		if(B[j+1]==A[i])j++;
		if(j==lb) {
			ans1[ans]=i-lb+1,ans++;
			j=next[j];
		} 
	}
}
int main(){
	cin>>t;
	for(int i=0;i<t;i++){
		ans=0;
		memset(ans1,0,sizeof(ans1));
		scanf("%s",A+1);
		scanf("%s",B+1);
		KMP();
		if(ans==0){ 
			cout<<"Not Found"<<endl<<endl;
		}else{
			cout<<ans<<endl;
			for(int j=0;j<ans;j++){
				cout<<ans1[j]<<" ";
			}
			cout<<endl<<endl;
		}
	}
	return 0;
} 

字符哈希

27博弈基础

巴什博弈

n个物品 两人轮流取1~m个 先取完者胜

n=k*(m+1)+r 先手取走r 必胜

n=k*(m+1) 先手必败

if(n%(m+1)) return false;
else return true

威佐夫博弈

两堆各有若干物品,可以在一堆拿若干(>1),也可以在两堆拿相同数量物品,先取完者胜

差值*黄金分割比==最小值,则后手赢

double r=(sqrt(5.0)+1)/2;
int d=abs(a-b)*r;
if(d!=min(a,b)) return true;
else return false;

尼姆博弈

有n堆石头,两人轮流从中取石头,每次取石头的个数>1,最后取完者胜

n堆物品数量全部异或,结果为0则必败,否则必胜

int res=0;
for(int i=1;i<=n;i++){
	res=res^a[i];
}
if(res) return true;
else return false;

SG函数

有向无环图游戏

  1. mex{ 集合 } 是集合里未出现的最小非负整数
  2. 注意:
    • 可选步数为1-m的连续整数,直接取模即可,SG(x) = x % (m+1);
    • 可选步数为任意步,SG(x) = x;
    • 可选步数为一系列不连续的数,用mex(计算每个节点的值)

【HDU1848】

#include<iostream>
#include<cstring>
using namespace std;
const int Maxm=1010,Maxn=10;
int f[20],sg[Maxm];
bool Hash[Maxm];
void Getsg()
{
	f[0]=1;f[1]=1;
	for(int i=2;i<=16;i++){//得到斐波那契数列
		f[i]=f[i-1]+f[i-2];
	}
	for(int i=1;i<Maxm;i++){//得到sg
		memset(Hash,false,sizeof(Hash));
		for(int j=1;j<=16&&f[j]<=i;j++){
			Hash[sg[i-f[j]]]=true;
		}
		for(int j=0;j<=i;j++){
			if(!Hash[j]){
				sg[i]=j;
				break;
			}
		}
	}
}
int main()
{
	Getsg();
	int m1,m2,m3;
	while(scanf("%d%d%d",&m1,&m2,&m3)==3&&m1){
		if((sg[m1]^sg[m2]^sg[m3])!=0){
			cout<<"Fibo"<<endl;
		}
		else{
			cout<<"Nacci"<<endl;
		}
	}
} 

28背包DP,树上DP (树上背包)

0 1 背包

【背包容量由大到小进行更新】

我们遍历每一种物品,每种物品都有不取两种选择。

初始化dp[0] [i] = 0, 其中 0 ≤ im

目标值dp[n] [m]

int dp[maxn][maxm];//用 dpi 表示遍历到第 i 件物品,且背包容量为 j 时的最大总价值。
 
void init() {//初始化
    for(int i = 0; i <= m; i++) {
     dp[0][i] = 0;
    }
}

for(int i=1;i<=n;i++){//遍历每一件物品  
	for(int j=m;j>=0;j--){//背包重量递减
		//如果放不下了肯定不能硬塞
        if(j < w[i]) {
            dp[i][j] = dp[i - 1][j]; 
        }
        //如果能放的下
        else {//就看看要不要放入这件物品 
            dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]); }
    	}
    }
}

printf("%d", dp[n][m]);//输出答案

改进代码:

int dp[maxn];//表示容量为maxn是的最大价值

memset(dp,0,sizeof(dp));//初始化

for(int i=1;i<=n;i++ ){//遍历每一种物品
	for(int j=m;j> w[i];j--){{//注意j一定要由大到小更新,且下界为w[i]
		dp[j]= max(dp[j], dp[j - w[i]] + v[i]);//dp[j]容量为j时不拿第i件物品  dp[j - w[i]] + v[i] 容量为j时拿第i件物品
    }
}

printf("%d",dp[m]);

完全背包

与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次

dp[i] [j]:表示前i件物品放入容量为j的背包的最大价值

背包重量从小到大枚举

dp[i] [j] = max(dp[i-1] [j], dp[i] [j-w[i]]+v[i])

for(int i=1;i<=n;++i)  //遍历物品数量
{
	for(int j=0;j<=t;++j)  //遍历背包容量  递增
	{
		dp[i][j] = dp[i-1][j];  //继承上一个背包
		if(j>=v[i])
		{
			//完全背包状态转移方程
			dp[i][j] = std::max(dp[i-1][j],dp[i][j-v[i]]+w[i]); 
		} 
	} 
}

改进:

for(int i=1;i<=n;++i)  //遍历物品数量
{
	for(int j=0;j<=t;++j)  //遍历背包容量
	{
		dp[i] = dp[j];  //此时右边的dp[j]是上一层i-1的dp[j],然后赋值给了当前i的dp[i]
		if(j>=v[i])
		{
			//完全背包状态转移方程
			dp[j] = std::max(dp[j],dp[j-v[i]]+w[i]);
		} 
	} 
} 

多重背包

与 0-1 背包的区别在于每种物品有 k个,而非一个

dp[i] [j]:表示前i件物品放入容量为j的背包的最大价值

dp[i][j] = dp[i-1] [j-k*v[i]] + k *w[i]

for(int i=1;i<=n;i++)   //遍历物品数量 
{
	for(int j=0;j<=m;j++)  //遍历背包容量 
	{
		for(int k=0;k<=ki && k*v[i]<=j;k++)  //遍历每个物品的件数 
		{
			//k*v[i]:不能超过背包总容量 
			dp[i][j]=std::max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*w[i]);
		}
	}
}

混合背包

有的只能取一次,有的能取无限次,有的只能取 k次。

树上背包

满足如果选取节点 v,则其所有祖先节点 u 都要选择的限制

边权题胚

给定一棵树。每条有对应的非负权值 val

现今需要剪枝。给定需要保留的边的数量 m,求所剩边的最大权值之和。

点权题胚

给定一棵树。每个节点有对应的非负权值 val

现今需要剪枝。给定需要保留的节点数量 m,求所剩节点的最大权值之和。

void dfs(int x,int father)
{
	for(int i=h[x];i!=-1;i=ne[i])//用链式前向星存树
	{
		int j=e[i];
		//树是一种有向无环图
		//只要搜索过程中不返回父亲节点即可保证不会重复遍历同一个点。
		if(j==father) continue;
		dfs(j,x);//继续搜索下一个节点
	}
}

void dfs(int u,int fa)
{
	for(int i=head[u];i;i=e[i].pre)
	{
		int y = e[i].to;
		if(y==fa) continue;
		dp[y][1] = e[i].w;  //把权重放在孩子节点
		dfs(y,u);  //向下递归 
		
		for(int j=m+1;j>=1;--j)  //根节点必选,故j>=1 
		{
			for(int k=0;k<j;++k)
			{
				dp[u][j] = std::max(dp[u][j],dp[u][j-k]+dp[y][k]);
			}
		}
	}
}

树的直径

int d[maxn];   //d[x]:节点x到其子孙节点的最大距离
int f[maxn];   //f[x]:以x为根结点的一条最长路径的距离

void dfs(int u,int fa)
{
	for(int i=head[u];i;i=e[i].pre)
	{
		int v = e[i].to;
		int w = e[i].w;
		if(v==fa) continue;
		dfs(v,u);
		f[u] = max(max(f[u],d[u]),max(d[v]+w,d[u]+d[v]+w));
		d[u] = max(max(d[u],w),d[v]+w);
	}
}

最大子树和

给定一棵有 n 个点的树,树上每个点有各自的点权。(存在负值)

在树中选取一棵子树,使得其权值之和最大。

dp[i] 表示以 i 为根节点的最大子树和。

dp[u] = val[u] + ∑max(dp[to], 0)

void dfs(int u, int father) {
	dp[u] = val[u]; //初始化
    
    for(int i = head[u]; i != -1; i = edge[i].next) { //遍历u的所有边
        int to = edge[i].to; //取出节点
        if(to == father) { //不要回头搜 
            continue; 
        }
        dfs(to, u); //递归过程
        if(dp[to] > 0) { //不使答案劣化
            dp[u] += dp[to]; //才参与贡献
        }
    }
}
dfs(1, -1); //根节点和它不存在的父节点
int ans = ninf; //负无穷
for(int i = 1; i <= n; i++){
	ans = max(ans, dp[i]);
}
printf("%d", ans); //输出

最大(权值)独立集

最大独立集定义:

对于一棵有 n 个结点的树,选出尽量多的结点,使得任两个结点均不相邻。

给定一棵有 n 个结点的树,及每个节点的权值 val

选择合适的节点,使得节点权值之和最大,并且任两节点不相邻

dp[i][0] 表示不选择节点 i 时,以节点 i 为根的子树的最大独立集的权值。

dp[i][1] 表示选择节点 i 时,以节点 i 为根的子树的最大独立集的权值。

那么转移方程有:

i. dp[u] [0] = ∑max(dp[to] [0], dp[to] [1])

ii. dp[u] [1] = val[u] + ∑dp[to] [0]

void dfs(int u, int father) { 
    //初始化 
    dp[u][0] = 0; 
    dp[u][1] = val[u]; 
    for(int i = head[u]; i != -1; i = edge[i].next) { 
        int to = edge[i].to; 
        if(to == father) { 
            continue; 
        }
        dfs(to, u); 
        dp[u][0] += max(dp[to][0], dp[to][1]); 
        dp[u][1] += dp[to][0]; 
    } 
}
  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值