CSP - J 精讲营 讲课笔记

Day 1

数据结构

  • 含义

    拿来存储数据的结构

  • 常见数据结构:

变量

只能存一个数。

数组

所有数组都应该开在 全局变量

  • 栈空间

局部变量都在栈空间。

空间为 \(64\) K,只能存 \(16384\)int

  • 堆空间

全局变量都在堆空间。

内存限制都是针对堆空间的。

空间为 \(256\) M ,可以存大约 \(6.4 × 10^7\)int

关于数组越界

假设:int a[10],如果我们访问 a[20]a[-3],不一定会 RE,但是有可能会出现一些奇奇怪怪的错误,因为不知道访问到的这个地方是哪里,可能这个位置已经分配给了其他变量。

例题 1

计算 \(n\) 个数的和。

#include<bits/stdc++.h>
using namespace std;
int n,ans,a[1005];
signed main(){
	cin>>n;
	//int a[n+10]; 不能定义为局部变量(栈空间),一旦 n 过大就会爆栈
	for(int i=1;i<=n;i++){
		cin>>a[i];
		ans+=a[i];
	}
	cout<<ans<<endl;
	return 0;
}

例题 2

使用负数下标:

#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int *b=a+500000; 
signed main(){
	b[-233];//指向 a[500000 - 233] 
	//b 的范围为 -500000 ~ 500000]; 
} 

(其实也可以用 map,但是 map 太慢了)

队列

基本性质:
FIFO(first in first out):先进先出。

STL 队列

#include<bits/stdc++.h>
using namespace std;
queue<int> q;//queue<队列内元素的类型> 队列名称
signed main(){
	q.push(233);
	q.push(2333);//扔一个数到队列里面
	
	q.pop();//让队列最左边的数走掉 233
	
	int v=q.front();//询问队列最左边的数是谁 2333
	
	q.size();//询问队列里面还有几个数 1
	
	q.empty();//判断队列是否为空 false 
} 

手写队列

#include<bits/stdc++.h>
using namespace std;
struct shouxie_queue{
	int z[1000005];
	int head;//第一个人在什么位置 
	int tail;//最后一个人在什么位置
	
	shouxie_queue(){//构造函数 
		head=1;
		tail=0;
		memset(z,0,sizeof(z)); 
		/*
			memset(z,1,sizeof(z)) ×
			可以用的:-1 0x3f 0x3f3f3f3f(无穷大) 
		*/ 
	}
	
	void push(int x){
		z[++tail]=x;
	}
	
	void pop(){//在外面调用时判空 
		head++; 
	}
	
	int front(){
		return z[head];
	}
	
	int size(){
		return tail-head+1;
	}
};

B3616

洛谷链接: B3616 【模板】队列
非常简单的一道模板题,直接用 \(STL\) 实现即可。

#include<bits/stdc++.h>
using namespace std;
queue<int> q;
int n;
signed main(){
	cin>>n;
	while(n--){
		int op;
		cin>>op;
		if(op==1){//加入元素 
			int x;
			cin>>x;
			q.push(x);
		}
		else if(op==2)//将队首弹出 
			if(q.empty()){
				cout<<"ERR_CANNOT_POP"<<endl;
				continue;
			}
			q.pop();
		}
		else if(op==3){//查询队首 
			if(q.empty()){
				cout<<"ERR_CANNOT_QUERY"<<endl;
				continue;
			}
			cout<<q.front()<<endl;
		}
		else cout<<q.size()<<endl;//查询队列元素 
	}
	return 0;
}

基本性质:
先进后出(一个桶)。

STL 栈

#include<bits/stdc++.h>
using namespace std;
stack<int> s;//stack<栈内元素的类型> 栈名称
signed main(){
	s.push(233);
	s.push(2333);//扔一个数到栈里面
	
	q.pop();//让栈最上边的数走掉 2333
	
	int v=s.top();//询问栈最上边的数是谁 233
	
	s.size();//询问栈里面还有几个数 1(233) 
	
	s.empty();//判断队列是否为空 false  
}

B3614

洛谷链接:B3614 【模板】栈
同样是模板题,用 \(STL\)
不过这个题非常坑有三个坑点:

  1. $x \lt 2^{64} $,所以定义的栈和 $ x $ 都要用 unsigned long long
  2. \(1 \leq T, n\leq 10^6\),所以我们的 endl 要换成 \n,否则会 T。
  3. 多测不清空,爆零两行泪!所以每次要将定义的栈清空
#include<bits/stdc++.h>
using namespace std;
stack<unsigned long long> h;//坑点一 
int T;
signed main(){
	cin>>T;
	for(int i=1;i<=T;i++){
		int n;
		cin>>n;
		while(!h.empty()) h.pop();//坑点三 
		for(int i=1;i<=n;i++){
			string s;
			cin>>s;
			if(s=="push"){
				unsigned long long x;//坑点一 
				cin>>x;
				h.push(x);
			}
			else if(s=="pop"){
				if(h.empty()){
					cout<<"Empty\n";//坑点二 
					continue;
				}
				else h.pop();
			}
			else if(s=="query"){
				if(h.empty()){
					cout<<"Anguei!\n";//坑点二
					continue;
				}
				else cout<<h.top()<<'\n';//坑点二
			}
			else cout<<h.size()<<'\n';//坑点二
		}
	}
	return 0;
} 

例题 3:

给你 $ n $ 个数,计算所有长度为 $ k $ 的区间的最大值和最小值的差为多少,共输出 $ n - k + 1 $ 个数。

暴力只要暴力搞得好,拿一等奖没烦恼()

#include<bits/stdc++.h>
using namespace std;
int n,k,a[1000005];
signed main(){
	cin>>n>>k;
	
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	//O((n-k+1)*k) = O(nk) 	
	for(int i=1;i<=n-k+1;i++){//枚举每个区间的左端点 
		int minn=a[i],maxn=a[i];//这 k 个数的最小值和最大值
		for(int j=0;j<k;j++){//for(int j=i;j<=i+k;j++)
			minn=min(minn,a[i+j]);//a[j]
			maxn=max(maxn,a[i+j]);//a[j]
		} 
		cout<<maxn-minn<<endl;
	}
	
	return 0;
}

$ k \leq n \leq 10^6 $,该代码为 \(O(nk)\) 的时间复杂度,也就跑一个小时吧()

正解:

单调队列

因为要从后面删除及询问,所以要用手写队列。
用单调递增队列来求最小值,用单调递减队列来求最大值。

P1886

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

#include<bits/stdc++.h>
using namespace std;
int n,k;
int a[1000005];
struct queuee{
	int z[1000005],head,tail;
	queuee(){ 
		head=1,tail=0;
		memset(z,0,sizeof(z)); 
	}
	void push1(int x){//单调递增 
		while(head<=tail&&a[x]<=a[z[tail]]){
			//1. 保证队列不为空
			//2. 最后一个数比加进来的数大 干掉
			tail--;
		}
		z[++tail]=x;
	}
	void push2(int x){//单调递减 
		while(head<=tail&&a[x]>=a[z[tail]]){
			tail--;
		}
		z[++tail]=x;
	}
	void pop(){
		head++;
	} 
	int front(){
		return z[head];	
	}	
	int size(){
		return tail-head+1;
	}
};

queuee q1,q2;
signed main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	for(int i=1;i<=k;i++)
		q1.push1(i),q2.push2(i);
	cout<<a[q1.front()]<<' ';//第一个区间的答案
	for(int i=2;i<=n-k+1;i++){//区间开始位置 
		q1.push1(i+k-1);//右边多一个数
		if(q1.front()==i-1)q1.pop();//左边扔一个数
		cout<<a[q1.front()]<<' ';//第 i 个区间的答案 
	} 
	
	cout<<endl;
	cout<<a[q2.front()]<<' ';
	for(int i=2;i<=n-k+1;i++){
		q2.push2(i+k-1);
		if(q2.front()==i-1)q2.pop();
		cout<<a[q2.front()]<<' ';
	} 
	return 0;
}

P5788

洛谷链接:P5788 【模板】单调栈
这道题普通的 cin cout 会 TLE,可以改用 scanf printf,这里我开的 O2。

#include<bits/stdc++.h>
using namespace std;
int n,a[3000006],ans[3000006];
stack<int> s;
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	for(int i=n;i>=1;i--){//倒序枚举 
		while(!s.empty()&&a[s.top()]<=a[i])s.pop();//比它小的就删去 
		if(s.empty())ans[i]=0;//没有比它小的就是0 
		else ans[i]=s.top();//否则就是栈顶元素 
		s.push(i);//不要忘记把每个数放进栈中 
	}
	
	for(int i=1;i<=n;i++)//正序输出答案 
		cout<<ans[i]<<' ';
	return 0;
} 

优先队列(堆)

  • 可实现操作:
  1. 加数
  2. 大根堆删最大值,小根堆删最小值
  3. 大根堆求最大值,小根堆删最小值
  • 小根堆

默认定义为大根堆,小根堆实现有两种方法:

  1. 存数时改变为相反数
  2. priority_queue<int,vector<int>,greater<int> >q;

STL 堆:

#include<bits/stdc++.h>
using namespace std;
priority_queue<int> heap;//大根堆 
signed main(){
	heap.push(233);//O(log n) 
	heap.push(2333);//向堆里面扔一个数
	
	heap.pop();//扔掉最大值 2333 O(log n)
	
	heap.top();//询问最大值 233 O(1)
	
	heap.size();//询问堆的大小 O(1)
}

log

插一个 $ log $ 的介绍:
$ a^b = c , log_a c = b$

特殊性质:

  1. 默认 $ a=2 $
  2. \(log_a b + log_a c=log_a bc\)

手写堆(默认大根堆):

#include<bits/stdc++.h>
using namespace std;
struct heap{//大根堆 
	int n;//当前堆里面总共有 n 个数
	int a[1000005];
	 
	int top(){//询问最大值 
		return a[1] 
	}
	
	int size(){//求堆内元素的数量 
		return n; 
	}
	
	int push(int x){//向堆中加入数 x 
		a[++n]=x;
		int p=n;
		while(p!=1){
			int f=p/2;
			if(a[f]<a[p]){
				swap(a[f],a[p]);
				p=f;
			}
			else break;
		}
	}
	
	void pop(){//删除最大值 
		a[1]=a[n--];
		int p=1;
		while(){
			int pp=p*2;//左儿子 
			if(pp+1<=n&&a[pp+1]>a[pp])pp++;//pp指向两个数中最大的那个数 
			if(a[p]<a[pp]){
				swap(a[p],a[pp]);
				p=pp;
			}
			else break;
		}
	}
}; 

P3378

洛谷链接:P3378 【模板】堆
当然可以用 $ STL $ 来实现,这里我用的是手写堆。

#include<bits/stdc++.h>
using namespace std;
int h[10000005];
int n;
int len;//队列的长度(堆的数据个数) 
void push(int x){
	//把x放到队尾
	len++;
	h[len]=x;
	//循环比较 当前节点与它的父节点
	int i=len;//当前节点
	int j=i/2;//当前节点的父节点
	while(h[i]<h[j]&&j>=1){
		swap(h[i],h[j]);
		i=j;
		j=i/2;
	} 
}
void pop(){
	h[1]=h[len];
	len--;
	int i=1;//父节点
	int j=2*i;//左儿子
	if(h[j]>h[j+1]&&j+1<=len) j=j+1;//较小值
	while(h[i]>h[j]&&j<=len){
		swap(h[i],h[j]);
		i=j;
		j=2*i;
		if(h[j]>h[j+1]&&j+1<=len) j=j+1;//较小值
	} 
} 
int top(int x){
	return h[1];
}
int size(int x){
	return len;
}
bool empty(){
	if(len==0)return true;
	else return false;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		if(x==1){
			int y;
			cin>>y;
			push(y);
		}
		else if(x==2)cout<<top(x)<<endl;
		else pop();
	}
	return 0;
}

P1168

洛谷链接:P1168 中位数

给定 $ n $ 个数,枚举每个区间,输出每个区间的中位数。$ n \leq 2000 $。
中位数:设枚举的区间的长度为 $ x $。

  • x%2==1 时,中位数为 sort 后的 x/2+1
  • x%2==0 时,中位数为 sort 后的 x/2-1x/2 两数的平均值。
#include<bits/stdc++.h>
using namespace std;

int n,a[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	for(int l=1;l<=n;l++){
		priority_queue<int> heap_l;//存较小的那部分数 大根堆 
		priority_queue<int> heap_r;//存较大的那部分数 小根堆
		heap_l.push(a[l]);
		cout<<l<<"~"<<l<<"的中位数为"<<a[l]<<endl; 
		int median=a[l];//当前中位数 
		
		for(int r=l+1;r<=n;r++){//l~r这一段的中位数 
			if(a[r]>median)heap_r.push(-a[r]);
			else heap_l.push(a[r]);
			
			while(heap_l.size()>heap_r.size+1){
				int v=heap_l.top();
				heap_l.pop();
				heap_r.push(-v);
			} 
			
			while(heap_r.size()>heap_l.size+1){
				int v=heap_r.top();
				heap_r.pop();
				heap_l.push(-v);
			} 
			
			if(heap_l.size()>heap_r.size)median=heap_l.top();
			else if(heap_r.size()>heap_l.size())median=-heap_r.top();
			else median=(heap_l.top()-heap_r.top())/2;//小根堆存的相反数,所以-
			
			cout<<l<<"~"<<r<<"的中位数为"<<median<<endl; 
		}
	}
} 

map

  • 本质
    无限大的万能数组。
  • 基本定义形式
    map<下标类型,元素类型> map名称
  • 缺点
    越高级越慢。普通数组访问一个值是 $ O(1) $ 的时间复杂度,而 map 则是 log 级别的。
#include<bits/stdc++.h>
using namespace std;

map<int,int> a;
//第一个 i 代表下标类型
//第二个 i 代表元素类型

map<string,string> b;
map<string,int> c;
map<int,string> d;

signed main(){
	a[1]=2;//log 
	a[2147483647]=0;//log
	a[-2333]=9;//log
	
	b=["hello"]="world"; 
	
	c["apple"]=1;
	d[1]="apple";
} 

pair

请移步此处

并查集

\(n\) 个点,每个点都有一条指向自己的边。
添加 \(i\)\(j\) 的边,就把 \(i\) 指向自己的那条边掰开,指向 \(j\)
如果 \(i\) 上指向自己的边已经掰开,我们就把与 \(i\) 联通的点的指向自己的边掰开,指向 \(j\)

int go(int p){//看一下点 p 沿着并查集箭头最后会走到哪里 
	if(to[p]==p)return p;//指向自己
	else return go(to[p]);//递归调用 
} 

详见 Kruscal 部分。

如果两个点在并查集中可以不断走向同一个点,那么这两个点就属于同一个连通块。

算法

对变量的算法

#include<algorithm>
//#include<bits/stdc++.h>
using namespace std;
signed main(){
	int a,b;
	min(a,b);//求两个数的最小值
	max(a,b);//求两个数的最大值
	swap(a,b);//交换两个数
	min(a,min(b,c));//求三个数的最小值
	
	int m;
	long long n;
	min(m,n);//会 CE,min、max、swap 要求带入的两个数类型完全一致 
}

对数组的算法

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

int a[23333];
bool cmp(int a,int b){
	return a>b;
}

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	sort(a+1,a+n+1);//把a[1]~a[n]从小到大排序 O(n log n)
	sort(a+1,a+n+1,cmp);//把a[1]~a[n]从大到小排序
	reverse(a+1,a+n+1);//翻转a[1]~a[n]
	
	quick_sort(a+1,a+n+1);//快速排序 
	merge_sort(a+1,a+n+1);//归并排序 
	heap_sort(a+1,a+n+1);//堆排序
	
	unique(a+1,a+n+1);//把a[1]~a[n]去重 必须先排序再调用
	int m=unique(a+1,a+n+1)-a-1;//如果去重完有x个数,会返回a+x+1 
	
	random_shuffle(a+1,a+n+1);//把a[1]~a[n]随机打乱 
}	 

归并排序

核心思想就是分治,分完再治。

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

int n,a[1000005],b[1000005];
void merge_sort(int l,int r){//对a[l]~a[r]进行归并排序 
	if(l==r)return; 
	int m=(l+r)/2;
	//分 
	merge_sort(l,m);//左边部分 
	merge_sort(m+1,r);//右边部分
	//治 
	int pl=l;//左边第一个数的下标(位置)
	int pr=m+1;//右边第一个数的下标(位置)
	
	for(int i=l;i<=r;i++){
		if(pl>m)b[i]=a[pr++];//左边没数了 
		else if(pr>r)b[i]=a[pl++];//右边没数了 
		else if(a[pl]<a[pr])b[i]=a[pl++];//左边更小 
		else b[i]=a[pr++];//右边更小 
	} 
	
	for(int i=l;i<=r;i++)
		a[i]=b[i];
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	merge_sort(1,n);
	
	for(int i=1;i<=n;i++)
		cout<<a[i]<<' '; 
}

逆序对

P1908

洛谷原题:P1908 逆序对

给你 $ n $ 个数,问有多少个逆序对?其中 $ n \leq 10^5 $。
逆序对:一个二元组 \({i,j}\),满足 $ i < j $ 且 $ a_i > a_j $(即前面的数大于后面的数)。

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

int n,a[1000005],b[1000005];
long long merge_sort(int l,int r){//对a[l]~a[r]进行归并排序 
	if(l==r)return 0; 
	int m=(l+r)/2;
	//分 
	long long ans=0;
	ans+=merge_sort(l,m);//左边部分 
	ans+=merge_sort(m+1,r);//右边部分
	//治 
	int pl=l;
	int pr=m+1;
	
	for(int i=l;i<=r;i++){
		if(pl>m)b[i]=a[pr++];//左边没数了 
		else if(pr>r)b[i]=a[pl++];//右边没数了 
		else if(a[pl]<=a[pr])b[i]=a[pl++];//左边更小 
		else b[i]=a[pr++],ans+=m-pl+1;//右边更小 
	} 
	
	for(int i=l;i<=r;i++)
		a[i]=b[i];
	return ans;
}
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	long long ans=merge_sort(1,n);
	
	cout<<ans<<endl;
}

前缀和

题面:给定 $ n $ 个数,$ T $ 次操作,每次给定 $ l , r $,问第 $ l $ 个数到第 $ r $ 个数的和。

#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],sum[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=aum[i-1]+a[i];
	}
	
	int T;
	cin>>T;
	while(T--){
		int l,r;
		cin>>l>>r;
		cout<<sum[r]-sum[l-1]<<endl;
	}
	
	return 0;
}

差分

给定 $ n $ 个数,$ T $ 次操作,每次给定 \(l,r,v\),让第 $ l $ 个数到第 $ r $ 个数都加上 \(v\),问最后每个数的值。

#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],sum[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	int T;
	cin>>T;
	while(T--){
		int l,r,v;
		cin>>l>>r>>v;
		sum[l]+=v;
		sum[r+1]-=v;//计算差值
	}
	
	for(int i=1;i<=n;i++)
		sum[i]+=sum[i-1];//前缀和维护
	for(int i=1;i<=n;i++)
		cout<<a[i]+sum[i]<<' ';//差值+原值
	return 0;
}

P2367

洛谷链接:P2367 语文成绩
差分的模板题。

#include<bits/stdc++.h>
using namespace std;
int n,T,a[5000005],sum[5000005];
signed main(){
	cin>>n>>T;
	for(int i=1;i<=n;i++)
		cin>>a[i];

	while(T--){
		int l,r,v;
		cin>>l>>r>>v;
		sum[l]+=v;
		sum[r+1]-=v;
	}

	for(int i=1;i<=n;i++)
		sum[i]+=sum[i-1];
	
	int minn=500;//minn到200即可
	
	for(int i=1;i<=n;i++)
		minn=min(a[i]+sum[i],minn);

	cout<<minn<<endl;
	return 0;
}

快读快输

这里直接放一下我的快读快输的模板。

#define re register
#define gc getchar
#define pt putchar

inline int read(){
	re int x=0;re bool f=0;static char ch=gc();
	while(!(ch>=48&&ch<=57)&&ch!=EOF){if(ch=='-')f=1;ch=gc();}
	while(ch>=48&&ch<=57){x*=10;x+=(ch-48);ch=gc();}
	if(f)return -x;else return x;
}

void print(int x){
	char st[105];re int top=0;
	if(x==0)putchar('0');if(x<0)pt('-');
	while(x){if(x>0)st[++top]=x%10+48;else st[++top]=-(x%10)+48;x/=10;}
	while(top)pt(st[top--]);
}

(其实快输还没有 cout 快)

时间复杂度的优化

  • 顺序赋值比随机顺序赋值更快。
  • 几种运算符的速度比较(由快到慢):
    1. &&,||:1
    2. <=,>=,==,>,<,!=:32
    3. =:32
    4. 位运算:&,|,^,<<,>>:64
    5. +,-:96
    6. *:1024
    7. /,%:3w

(数字仅代表其速度之比)。

位运算

位运算均为在二进制下的运算。

  • &
    都是 \(1\) 就是 \(1\),其余为 \(0\)
    例:10&12=1010&1100=1100=8

  • |
    只要有 \(1\) 就是 \(1\),其余为 \(0\)
    例:10|12=1010|1100=1110=14

  • ^
    不同为 \(0\),相同为 \(1\)
    例:10^12=1010^1100=0110=6

  • <<
    x<<y=x*pow(2,y)

  • >>
    x>>y=x/pow(2,y)

Day 2

倍增

例题 4

给你一个长度为 \(n\) 的数组,有 \(T\) 次询问,每次给定 \(l,r\),求 \(a_l\)\(a_r\) 这个区间内的最大值。

  • 核心思想:
    \(f_{i,j}\) 代表从 \(a_i\) 开始的 \(2^j\) 个数的最大值。

  • 小技巧:
    \(n\) 个数的什么东西,区间太长,一般会在中间“砍一刀”,最后只保留一个数。

  • 算法(处理 \(f_{i,j}\)):
    \(f_{i,0}=a_i\)
    \(f_{i,j}=max(f_{i,j-1},f_{i+2^{j-1},j-1})\)

即:
f[i][0]=a[i]
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1])

  • 做法(用 \(f_{i,j}\) 来算答案):
    性质:一个数被算多次在求最大值中也不会影响结果。

具体做法:一个区间长为 \(len\),寻找两个为 \(2\) 的整数倍的数,使得这两个数相加大于 \(len\),让 \(j\) 的值分别等于这两个数。

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

//n<=100000 
//所以f的第二个维度只需要保证2^j>=n即可 
int n,a[100005],f[100005][20];
int x[100005]; 
//x[i]代表长度为i的区间 用两个长度为2^x[i]的区间能够覆盖 
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	x[1]=0;
	for(int i=2;i<=n;i++)
		x[i]=x[i>>1]+1;//算j 
		//x[111]=x[55]+1 
		
	for(int i=1;i<=n;i++)
		f[i][0]=a[i];
		
	for(int j=1;(1<<j)<=n;j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);//预处理 
	
	int T;
	cin>>T;
	while(T--){
		int l,r;
		cin>>l>>r;
		int len=r-l+1;
		cout<<max(f[l][x[len]],f[r-(1<<x[len])+1][x[len]])<<endl;//算结果 
	}
}

P3865

洛谷链接:P3865 【模板】ST 表
只要注意快读即可。

#include<bits/stdc++.h>
#define re register
#define lll __int128
#define gc getchar
#define pt putchar
using namespace std;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
void print(int x){
	char st[105];re int top=0;
	if(x==0)putchar('0');if(x<0)putchar('-');
	while(x){if(x>0)st[++top]=x%10+48;else st[++top]=-(x%10)+48;x/=10;}
	while(top)putchar(st[top--]);
}
void println(int x){
	print(x);putchar('\n');
}
int n,a[100005],f[100005][20],T;
int x[100005]; 
signed main(){
	n=read();
	T=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
		
	x[1]=0;
	for(int i=2;i<=n;i++)
		x[i]=x[i>>1]+1;//算j 
		//x[111]=x[55]+1 
		
	for(int i=1;i<=n;i++)
		f[i][0]=a[i];
		
	for(int j=1;j<=x[n];j++)
		for(int i=1;i+(1<<j)-1<=n;i++)
			f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);//预处理 
	
	while(T--){
		int l,r;
		l=read();
		r=read();
		int len=r-l+1;
		println(max(f[l][x[len]],f[r-(1<<x[len])+1][x[len]]));//算答案 
	}
}

二分

例题 5

在已经保证排好序的 \(n\) 个数的数列中,给定 \(T\) 次询问,每次询问 \(x\) 在该序列中是否出现

  • 暴力
    暴力查询即可。
#include<bits/stdc++.h>
using namespace std;
int n,a[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	sort(a+1,a+n+1);
	
	int T;
	cin>>T;
	
	while(T--){
		int x;
		cin>>x;
		int f=0;
		for(int i=1;i<=n;i++){
			if(a[i]==x){
				cout<<"Yes\n";
				f=1;
				break;
			}
		} 
		if(f==0)cout<<"No\n";
	}
	return 0;
}
  • 正解
    不断从中间砍一刀,看 \(x\) 在分界点的哪一边,注意二分要保证数列有单调性。
#include<bits/stdc++.h>
using namespace std;
int n,a[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	sort(a+1,a+n+1);
	
	int T;
	cin>>T;
	
	while(T--){
		int x;
		cin>>x;
		int l=0,r=n;
		//代表当前x可能出现的范围是 l+1~r
		
		while(l+1!=r){
			int m=(l+r)>>1;
			if(x<=a[m])r=m;//当前答案区间在左边,从 l+1~r 变为 l+1~m 
			else l=m;//当前答案区间在右边,从 l+1~r 变为 m+1~r		
		} 
		
		if(a[r]==x)cout<<"Yes\n";
		else cout<<"No\n";
	}
	return 0;
}

例题 6

题面:
\(n\) 首歌曲,\(b_i\) 表示第 \(i\) 首歌曲持续的时间。给定 \(T\) 次询问,问 \(t_i\) 时刻放的是哪首歌。

#include<bits/stdc++.h>
using namespace std;
int n,a[1000005],b[1000005];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>b[i];
	for(int i=1;i<=n;i++)
		a[i]=a[i-1]+b[i];//前缀和计算每首歌在何时开始播放 
		
	int T;
	cin>>T;
	
	while(T--){
		int x;
		cin>>x; 
		int l=0,r=n;
		
		while(l+1!=r){//二分 
			int m=(l+r)>>1;
			if(x<=a[m])r=m;//在左边 
			else l=m;//在右边	
		} 
		
		cout<<r<<endl; 
	}
	return 0;
}

例题 7

题面;
\(n\) 件衣服,每件衣服都有一定的含水量,每分钟每件衣服的含水量会减少 \(1\)。你有一个吹风机,每分钟只能对一件衣服使用吹风机,用吹风机可以使该衣服每分钟减少 \(k\) 的含水量。问最少需要多少分钟能让所有的衣服的含水量非正。

#include<bits/stdc++.h>
using namespace std;
int n,a[100005];

bool check(int t){//检查能否在t分钟把所有衣服弄干 
	int sum=0;
	for(int i=1;i<=n;i++)
		if(a[i]>t)sum+=(a[i]-t-1)/k+1;
	if(sum<=t)return true;
	else return false;
} 
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	sort(a+1,a+n+1);
	
	int l=0,r=a[n];
	while(l+1!=r){
		int m=(l+r)>>1;
		if(check(m))r=m;//如果m分钟能把所有衣服弄干,说明答案在 l+1~m
		else l=m;//否则答案在 m+1~r 
	} 
## ;	cout<<r<<endl;
	return 0;
## }}

小技巧

看到最小的最大值和最大的最小值之类的字眼就要用二分来算。

快速幂

int ksm(int x,int y,int p){//计算 x^y %p 
	if(y==0)return 1;
	int z=ksm(x,y/2,p);//z=x^(y/2) %p
	z=1ll*z*z%p;
	if(y&1)z=1ll*z*x%p;
	return z; 
}

矩阵

就是 \(n\)\(m\) 列的二维数组,用中括号框起来。

例如当 \(n = 2,m = 3\) 时,有一个矩阵 \(A\) 如下:

\[\begin{bmatrix} 1&2&3 \\ 4&5&6 \\ \end{bmatrix} \]

矩阵加减

将对应位置的两个元素相加,比较容易理解。

\[\begin{bmatrix} 1&2&3 \\ 4&5&6 \\ \end{bmatrix} + \begin{bmatrix} 1&2&3 \\ 4&5&6 \\ \end{bmatrix} = \begin{bmatrix} 2&4&6 \\ 8&10&12 \\ \end{bmatrix} \]

减法同理。

矩阵乘数

也是比较简单,用所有元素乘上这个数。

\[\begin{bmatrix} 1&2&3 \\ 4&5&6 \\ \end{bmatrix} \times 3 = \begin{bmatrix} 3&6&9 \\ 12&15&18 \\ \end{bmatrix} \]

矩阵乘矩阵

定义 \(A\)\(n\)\(m\) 列,\(B\)\(m\)\(k\) 列。

\[A= \begin{bmatrix} 1&2&3 \\ 4&5&6 \\ \end{bmatrix} \]

\[B= \begin{bmatrix} 1&2 \\ 3&4 \\ 5&6 \\ \end{bmatrix} \]

设答案矩阵为 \(C\),那么 \(C\) 一定是 \(n\)\(k\) 列。
具体来看做法:

\[\begin{aligned} & C_{1,1} = \text{A 矩阵中第 1 行第 1 个数} \times \text{B 矩阵中第 1 行第 1 个数} \\ & + \text{A 矩阵中第 1 行第 2 个数} \times \text{B 矩阵中第 2 行第 1 个数} \\ &+ \text{A 矩阵中第 1 行第 3 个数} \times \text{B 矩阵中第 3 行第 1 个数 } \\ & =1\times 1+2\times 3+3 \times 5 \\ & =1+6+15=22 \\ \end{aligned} \]

可以有技巧的算:\(C_{i,j} = \text{A 中的第 i 行(横向)的每一个数去依次乘上 B 中的每一行的第 j 个数(纵向)的和}\),说起来比较绕,但是懂得了规律以后会发现十分简单。

所以 \(C\) 矩阵如下:

\[\begin{bmatrix} 22&28 \\ 49&64 \\ \end{bmatrix} \]

矩阵乘法代码

struct Matrix{ 
    int n,m;//n:矩阵行数 m:矩阵列数 
    int a[105][105];//矩阵 
    Matrix(){//构造函数,作用:初始化矩阵为 0。
        n=m=0;
        memset(a,0,sizeof (a));
    }
}; 
Matrix operator * (const Matrix &a,const Matrix &b){
	//重载 * 运算,起作用当且仅当是 Matrix 类型
	//&:取地址符,操作的时候是直接修改两个矩阵,速度更快,防止拷贝 
	//const:防止运算的时候把运算的矩阵本尊破坏 
	//注意类型 
    Matrix ans;//返回的答案 
    int x=a.n,y=b.m;
    ans.n=x;
	ans.m=y; 
    for(int i=1;i<=x;i++)
        for(int j=1;j<=y;j++)
        	//枚举每一个位置
            for(int k=1;k<=a.m;k++)
            	//核心部分 
                ans.a[i][j]+=a.a[i][k]*b.a[k][j];
    return ans;
}
ans=a*b;
  • 用处
  1. 部分动态规划来加速。
  2. 快速解决线性递推的问题。

P1962

洛谷链接:P1962 斐波那契数列

#include<bits/stdc++.h>
#define ll long long
#define itn int
#define ull unsigned long long
#define gt getchar
#define pt putchar
using namespace std;
inline ll read(){
    ll x=0,f=1;char ch=gt();
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=gt();}
    while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=gt();}
    return x*f;}
inline void print(ll x){if(x<0)pt('-'),x=-x;if(x>9)print(x/10);pt(x%10+48);}
const ll M = 1e9 + 7;
ll n;
struct Matrix{
    ll a[105][105];//自行开空间 
    int n, m;
    Matrix() {memset(a, 0, sizeof a);}
}ans, base;
Matrix operator * (const Matrix &A, const Matrix &B){//& 直接调用A, B.const 防止 A, B 被修改 
        Matrix Ans;
        int n = A.n, m = B.m;
        Ans.n = n, Ans.m = m;
		for(int i =1; i <= n; ++i)
			for(int j = 1; j <= m; ++j)
				for(int k = 1; k <= A.m; ++k)
					Ans.a[i][j] = (Ans.a[i][j] + A.a[i][k] * B.a[k][j]) % M, Ans.a[i][j] %= M;
		return Ans;
    }
void init(){
	base.n = base.m = 2, ans.n = 2, ans.m = 2;
    base.a[1][1] = base.a[1][2] = base.a[2][1] = 1;
    ans.a[1][1] = ans.a[1][2] = 1;
}
void qpow(ll b){
    while(b){
        if(b & 1)ans = ans * base;
        base = base * base;
        b >>= 1;
    }
}
int main(){
    n = read();
    if(n <= 2)return puts("1"), 0;
    init();
    qpow(n - 2);
    cout << ans.a[1][1] % M << "\n";
    return 0;
}

P3390

洛谷链接:P3390 【模板】矩阵快速幂

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int P=1000000007;
ll n,k;
struct Matrix{
    ll a[105][105];
}A,I; 
Matrix operator*(const Matrix &a,const Matrix &b){
    Matrix ans;
    for(ll i=1;i<=n;i++)
		for(ll j=1;j<=n;j++)
			ans.a[i][j]=0;
    for(ll i=1;i<=n;i++)
        for(ll j=1;j<=n;j++)
            for(ll k=1;k<=n;k++){
            	ans.a[i][j]+=a.a[i][k]*b.a[k][j]%P;
            	ans.a[i][j]%=P;
			}
                
    return ans;
}

signed main(){
	cin>>n>>k;
	for(ll i=1;i<=n;i++)
		for(ll j=1;j<=n;j++)
			cin>>A.a[i][j];
	
	for(ll i=1;i<=n;i++)
		I.a[i][i]=1;
	while(k>0) {
		if(k%2==1)I=I*A;
		A=A*A;
		k=k>>1;
	}
	
	for(ll i=1;i<=n;i++){
		for(ll j=1;j<=n;j++)
			cout<<I.a[i][j]<<' ';
		cout<<'\n';
	}
		
	return 0;
}

矩阵乘法律

有结合律和分配律(左分配律 右分配律)
没有交换律。

贪心

  • 基本做法
    按照某种形式排序。

P1080

洛谷链接:P1080 [NOIP2012 提高组] 国王游戏

\(2\) 个元素的关系。
通过这个关系对 \(n\) 个元素进行排序。
拟阵

详见我博客内的题解

例题 8

题面:
有一个容量为 \(v\) 的山洞,第 \(i\) 个石头放在山洞里会占据 \(a_i\) 的空间,在搬运过程中至少需要 \(b_i\) 的空间,问能不能把所有石头放下。

还是先看如果只有两块石头,比较的就是max(a,b,a1+b2,a1+a2)max(b,a,a2+b1,a1+a2)

如果 a1+a2 最大,可以删掉,因为哪种方案都可以.

化简结果为 a1+b2,a2+b1

变个号就是 a1-b1,a2-b2

搜索

DFS

搜索到底。栈
题面:在 \(n\) 个数中选 \(m\) 个数使得和最大。

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

int n,m,a[1000005],ans;
void dfs(int now,int nownum,int nowsum){
	//当前要看第now个元素选不选
	//已经选了nownum个数
	//选了的nownum个数的和是nowsum 
	if(now>n){
		if(nownum==m) ans=max(ans,nowsum);
		return;
	} 
	dfs(now+1,nownum,nowsum);//不选
	dfs(now+1,nownum+1,nowsum+a[now]);//选 
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	dfs(1,0,0);
	
	cout<<ans<<endl;
	return 0;
}

BFS

一层一层向外拓展。队列。

从起点开始,路上有障碍物,到终点所需最小步数。

每个状态只被算一次,第一次最优。

#include<bits/stdc++.h>
using namespace std;
int n,m,a[1005][1005];
int sx,sy;//起点
int tx,ty;//终点 
int step[1005][1005];
//step[i][j]代表从起点走到(i,j)需要走多少步 
int dx[4]={-1,1,0,0};
int dy[4]={0,0,-1,1};
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>a[i][j];//a[i][j]==1代表有障碍物
			
	cin>>sx>>sy>>tx>>ty;
	memset(step,-1,sizeof(-1));
	step[sx][sy]=0;
	
	/*
		struct pair{
			int first;
			int second;
		};
	*/
	
	queue<pair<int,int> > q;//用来存能够向外进行拓展step的点
	q.push(make_pair(sx,sy)); 
	
	while(q.size()){
		int x=q.front().first;
		int y=q.front().second;
		q.pop();//当前的点为(x,y)
		
		for(int i=0;i<4;i++){//枚举上下左右四个方向 
			int xx=x+dx[i];
			int yy=y+dy[i];//从(x,y)走到了(xx,yy)
			
			if(xx>=1&&xx<=n&&yy>=1&&yy<=m&&a[xx][yy]!=1&&step[xx][yy]==-1){
				step[xx][yy]=step[x][y]+1;
				q.push(make_pair(xx,yy));
			}
			 
		}
	}
}

搜索的分类

  • 最优解问题
    最大最小。
  • 可行解问题
    一组解。
  • 解数量问题
    总共有几个解。

什么时候用什么搜索方式

只有求最优解并且求得一定是最小步数时(每次操作代价必须都为 \(1\))才用 BFS,其余都用 DFS。

优化搜索

剪枝

少搜一些无用状态。

  • 可行性剪枝
    如上的DFS可以在函数里加上。
	if(nownum>m)return;//可行性剪枝1,选的太多 
	if(nownum+n-now+1)return;//可行性剪枝2,选的太少
  • 最优性剪枝
    不可能成为最优解的筛去。
if(nousum+qzh[n]-qzh[now-1]<=ans)return;//最优性剪枝,qzh为前缀和数组 

卡时

原理:在即将 TLE 时输出答案并退出程序。

//clock()在ctime库里,看程序运行了多长时间,但由于系统的不同会使得单位不同 
if(1.0*clock()/CLOCKS_PER_SEC>0.9){
	cout<<ans<<endl;
	exit(0);//卡时 
}

但是除法太慢,所以把除法放到另一边,改为乘法,然后把小数变为整数。

if(100*clock()>90*CLOCKS_PER_SEC){
	cout<<ans<<endl;
	exit(0);//卡时 
}

换顺序搜索

玄学!

Day 3

DFS 优化

P1219

洛谷链接:P1219 [USACO1.5] 八皇后 Checker Challenge

  • 基础做法
#include<bits/stdc++.h>
using namespace std;

int n,ans,pos[1005];//pos[i]表示第 i 行的皇后放在了第 pos[i] 列 

void dfs(int now){//现在要去尝试放 now 行的皇后 
	if(now>n){
		ans++;
		return;
	}
	
	for(int j=1;j<=n;j++){//第 now 行的皇后放在第 j 列 
		bool able=true;
		for(int i=1;i<now;i++)//枚举第 i 行的皇后 
			if(pos[i]==j||abs(now-i)==abs(j-pos[i]))//不能在同列和同一斜行 
				able=false;
		if(able){
			pos[now]=j;//放在第 j 列 
			dfs(now+1); 
		} 
	}
}

signed main(){
	cin>>n;
	dfs(1);
	cout<<ans<<endl;
}
  • 优化
    用数组来减少时间复杂度,让每次查询时的时间复杂度减为 \(O(1)\)
#include<bits/stdc++.h>
using namespace std;

int n,ans;
int pos[1005];//pos[i]表示第 i 行的皇后放在了第 pos[i] 列 
bool col[1005];//col[i]表示第 i 列上有没有皇后
bool xie1[1005];//xie1[i]代表第 i 条从左上到右下的斜线有没有皇后
bool xie2[1005];//xie2[i]代表第 i 条从右上到左下的斜线有没有皇后 

void dfs(int now){//现在要去尝试放 now 行的皇后 
	if(now>n){
		ans++;
		return;
	}
	
	for(int j=1;j<=n;j++){//第 now 行的皇后放在第 j 列 
		int idx1=j-now+n;//从左上到右下的斜线编号 
		int idx2=now+j-1;//从右上到左下的斜线编号 
		if(!col[j]&&!xie1[idx1]&&!xie2[idx2]){
			pos[now]=j;
			col[j]=xie1[idx1]=xie2[idx2]=true;//已经有皇后 
			dfs(now+1); 
			col[j]=xie1[idx1]=xie2[idx2]=false;//把皇后换掉 
		} 
	}
}

signed main(){
	cin>>n;
	dfs(1);
	cout<<ans<<endl;
}

比上一次的代码快了 \(6\) 倍。

  • 对称优化
    因为是对称的,所以我们只需要枚举一半,然后结果 \(×2\) 即可。
#include<bits/stdc++.h>
using namespace std;

int n,ans;
int pos[1005];//pos[i]表示第 i 行的皇后放在了第 pos[i] 列 
bool col[1005];//col[i]表示第 i 列上有没有皇后
bool xie1[1005];//xie1[i]代表第 i 条从左上到右下的斜线有没有皇后
bool xie2[1005];//xie2[i]代表第 i 条从右上到左下的斜线有没有皇后 

void dfs(int now){//现在要去尝试放 now 行的皇后 
	if(now>n){
		ans++;
		return;
	}
	
	int up=n;
	if(now==1)up=up>>1;//只需要枚举一半
	for(int j=1;j<=up;j++){//第 now 行的皇后放在第 j 列 
		int idx1=j-now+n;//从左上到右下的斜线编号 
		int idx2=now+j-1;//从右上到左下的斜线编号 
		if(!col[j]&&!xie1[idx1]&&!xie2[idx2]){
			pos[now]=j;
			col[j]=xie1[idx1]=xie2[idx2]=true;//已经有皇后 
			dfs(now+1); 
			col[j]=xie1[idx1]=xie2[idx2]=false;//把皇后换掉 
		} 
	}
}

signed main(){
	cin>>n;
	dfs(1);
	cout<< (ans<<1) <<endl;//加括号
}

奇数版本的咕咕了。

  • 状态压缩优化
    用一个整数(转二进制)来存储数组的信息。
#include<bits/stdc++.h>
using namespace std;

int n,ans;
int pos[1005];//pos[i]表示第 i 行的皇后放在了第 pos[i] 列 

void dfs(int now,int s1,int s2,int s3){
	//现在要去尝试放 now 行的皇后 
	//s1的二进制下第 i 位为 0 代表第 i 列没有皇后,1 代表有皇后 
	//s2左上到右下的斜线 
	//s3右上到左下的斜线 
	
	if(now>=n){
		ans++;
		return;
	}
	
	int s=s1|s2|s3;
	for(int j=1;j<n;j++){//第 now 行的皇后放在第 j 列 
		if( ((s>>j)&1)==0)
		if(!col[j]&&!xie1[idx1]&&!xie2[idx2]){
			pos[now]=j;
			dfs(now+1,s1|(1<<j),s2|(1<<j)<<1,s3|(1<<j)>>1);  
		} 
	}
}

signed main(){
	cin>>n;
	dfs(0,0,0,0);
	cout<<ans<<endl;
}
  • 状压和对称结合优化
    复制粘贴再稍微改改即可,不放代码了。

BFS 优化

P1379

  • 洛谷链接
    P1379 八数码难题
    (本道例题的答案是 \(123456780\),P1379根据原题进行输出的改变即可)

  • 普通做法

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

int a[10][10];
int step[876543210+1];//step[i]代表走到状态 i 所需的时间 
int s;
int dx[4]={1,-1,0,0};
int dy[4]={0,0,1,-1};//偏移量 

signed main(){
	for(int i=1;i<=3;i++)
		for(int j=1;j<=3;j++){
			cin>>a[i][j];
			s=s*10+a[i][j];
		} 
		
	memset(step,-1,sizeof(step));
	step[s]=0;
	queue<int> q;
	q.push(s);
		
	while(q.size()){//BFS 
		int s=q.front();
		int cur_step=step[s];
		q.pop();
		
		int x,y;
		for(int i=3;i>=1;i--)
			for(int j=3;j>=1;j--){
				a[i][j]=s%10;
				s=s/10; 
				if(a[i][j]==0)x=i,y=j;
			}
			
		for(int d=0;d<4;d++){
			int xx=x+dx[d];
			int yy=y+dy[d];//(x,y) 和 (xx,yy) 交换
			if(xx>=1&&xx<=3&&yy>=1&&yy<=3){
				swap(a[x][y],a[xx][yy]);
				s=0;
				for(int i=1;i<=3;i++)
					for(int j=1;j<=3;j++)
						s=s*10+a[i][j];
				if(step[s]==-1){
					step[s]=cur_step+1;
					q.push(s);
				}
				swap(a[x][y],a[xx][yy]);
			} 
		}
	}
	cout<<step[123456780]<<endl;	
}
  • 存储优化
    普通做法需要 \(3\) 个多 \(G\) 的内存,所以我们要考虑优化空间,因为每个数只会出现一次,所以我们只需要存储 \(8\) 位数即可。
//350MB 
#include<bits/stdc++.h> 
using namespace std;

int a[10][10];
int step[87654321+1];//step[i]代表走到状态 i 所需的时间 
int s;
int dx[4]={1,-1,0,0};
int dy[4]={0,0,1,-1};
bool vis[9];//vis[i]代表 i 是否出现 

signed main(){
	for(int i=1;i<=3;i++)
		for(int j=1;j<=3;j++){
			cin>>a[i][j];
			if(i!=3||j!=3)s=s*10+a[i][j];
		} 
		
	memset(step,-1,sizeof(step));
	step[s]=0;
	queue<int> q;
	q.push(s);
		
	while(q.size()){
		int s=q.front();
		int cur_step=step[s];
		q.pop();
		
		int x,y;
		memset(vis,false,sizeof(vis));
		for(int i=3;i>=1;i--)
			for(int j=3;j>=1;j--){
				if(i!=3||j!=3){
					a[i][j]=s%10;
					s=s/10; 
					vis[a[i][j]]=true;
				}
			}
		for(int i=0;i<9;i++)
			if(!vis[i])a[3][3]=i;
		for(int i=1;i<=3;i++)
			for(int j=1;j<=3;j++)
				if(a[i][j]==0)x=i,y=j;
			
		for(int d=0;d<4;d++){
			int xx=x+dx[d];
			int yy=y+dy[d];//(x,y) 和 (xx,yy) 交换
			if(xx>=1&&xx<=3&&yy>=1&&yy<=3){
				swap(a[x][y],a[xx][yy]);
				s=0;
				for(int i=1;i<=3;i++)
					for(int j=1;j<=3;j++)
						if(i!=3||j!=3)s=s*10+a[i][j];
				if(step[s]==-1){
					step[s]=cur_step+1;
					q.push(s);
				}
 				swap(a[x][y],a[xx][yy]);
			} 
		}
	}
	cout<<step[12345678]<<endl;	
}
  • 进制优化
    因为我们发现 \(9\) 这个数没有出现过,所以我们用 \(9\) 进制来进行优化。改变时只需要将代码中所有的 \(10\) 改变为 \(9\) 即可。
//172MB 
#include<bits/stdc++.h> 
using namespace std;

int a[10][10];
int step[9*9*9*9*9*9*9*9+1];//step[i]代表走到状态 i 所需的时间 
int s;
int dx[4]={1,-1,0,0};
int dy[4]={0,0,1,-1};
bool vis[9];//vis[i]代表 i 是否出现 

signed main(){
	for(int i=1;i<=3;i++)
		for(int j=1;j<=3;j++){
			cin>>a[i][j];
			if(i!=3||j!=3)s=s*9+a[i][j];
		} 
		
	memset(step,-1,sizeof(step));
	step[s]=0;
	queue<int> q;
	q.push(s);
		
	while(q.size()){
		int s=q.front();
		int cur_step=step[s];
		q.pop();
		
		int x,y;
		memset(vis,false,sizeof(vis));
		for(int i=3;i>=1;i--)
			for(int j=3;j>=1;j--){
				if(i!=3||j!=3){
					a[i][j]=s%9;
					s=s/9; 
					vis[a[i][j]]=true;
				}
			}
		for(int i=0;i<9;i++)
			if(!vis[i])a[3][3]=i;
		for(int i=1;i<=3;i++)
			for(int j=1;j<=3;j++)
				if(a[i][j]==0)x=i,y=j;
			
		for(int d=0;d<4;d++){
			int xx=x+dx[d];
			int yy=y+dy[d];//(x,y) 和 (xx,yy) 交换
			if(xx>=1&&xx<=3&&yy>=1&&yy<=3){
				swap(a[x][y],a[xx][yy]);
				s=0;
				for(int i=1;i<=3;i++)
					for(int j=1;j<=3;j++)
						if(i!=3||j!=3)s=s*9+a[i][j];
				if(step[s]==-1){
					step[s]=cur_step+1;
					q.push(s);
				}
				swap(a[x][y],a[xx][yy]);
			} 
		}
	}
	cout<<step[25206070]<<endl;//输出12345678的九进制 
}
  • 阶乘优化
    但内存还是很大,所以我们还是要继续优化。我们发现到,想 \(22222222\) 这种数是不存在的,但还是占了我们的内存,所以我们要考虑去掉这些无用的数。举个例子:\(432156870\),除第一个数外,第 \(i\) 个数用小于等于 \(i\) 的数中未枚举到(未删去)的数量 乘上 \(9-i\) 的阶乘。
//13MB
#include<bits/stdc++.h> 
using namespace std;

int a[4][4],b[4][4];

int step[3265920+1];//step[i] 代表走到状态i所需要的最小步数 

int dx[4]={-1,1,0,0};
int dy[4]={0,0,-1,1};
bool vis[9];//vis[i]i这个数有没有出现过 
int fac[10];//fac[i] 代表i! 

signed main(){
	fac[0]=1;
	for (int i=1;i<=9;i++)
		fac[i]=fac[i-1]*i;//首先处理阶乘 
		
	int s=0;
	for (int i=1;i<=3;i++)
		for (int j=1;j<=3;j++)
			cin>>a[i][j];
	
	for (int i=3;i>=1;i--)
		for (int j=3;j>=1;j--){	
			vis[a[i][j]]=true;//标记已经遍历 
			int cnt=0;//查看前面有几个比它大的数 
			for (int k=0;k<a[i][j];k++)
				if(!vis[k]) cnt++;
			s=s+cnt*fac[(i-1)*3+j];//存储下来 
		}
			
	memset(step,-1,sizeof(step));
	step[s] = 0;
	queue<int> q;
	q.push(s);
	while(q.size()){
		int s=q.front();
		int cur_step=step[s];
		q.pop();
		int x,y;
		memset(vis,false,sizeof(vis));
		
		for (int i=3;i>=1;i--)
			for (int j=3;j>=1;j--){
					int cnt=s/fac[(i-1)*3+j]+1;
					for (int k=0;k<9;k++)
						if (!vis[k]){
							cnt--;
							if(cnt==0)a[i][j]=k;
						}
					s=s%fac[(i-1)*3+j];
				}
				
		for (int i=1;i<=3;i++)
			for (int j=1;j<=3;j++)
				if (a[i][j]==0) x=i,y=j;
		for (int d=0;d<4;d++){
			int xx=x+dx[d];
			int yy=y+dy[d];//(x,y) 和 (xx,yy)交换
			if (xx>=1&&xx<=3&&yy>=1&&yy<=3){
				swap(a[x][y],a[xx][yy]);
				s=0;
				memset(vis,false,sizeof(vis));
				for (int i=3;i>=1;i--)
					for (int j=3;j>=1;j--){
						vis[a[i][j]]=true;
						int cnt=0;
						for(int k=0;k<a[i][j];k++)
							if(!vis[k]) cnt++;
						s=s+cnt*fac[(i-1)*3+j];
					}
				if (step[s]==-1){
					step[s]=cur_step+1;
					q.push(s);
				}
				swap(a[x][y],a[xx][yy]);
			} 
		}
	}
	cout<<step[462331]<<endl;//算出123456780的阶乘结果即可 
}
  • 目标状态 break 优化
    我们考虑最优性剪枝。当我们搜索到了答案后就可以直接 break 掉,这里就不放代码了。

  • 双向 BFS 优化
    当然了,还可以优化,我们可以从起点和终点同时进行遍历,当遍历到的点相同时,两边所需的步数加起来就是我们要求的答案。

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

int a[4][4],b[4][4];

int step[87654321+1];//step[i] 代表走到状态i所需要的最小步数 
int belong[87654321+1];
//belong[i]==0 代表这个状态还没有被搜到
//belong[i]==1 代表这个状态是从起点被搜到的
//belong[i]==2 代表这个状态是从终点被搜到的 

int dx[4]={-1,1,0,0};
int dy[4]={0,0,-1,1};
bool vis[9];//vis[i]i这个数有没有出现过 

signed main(){
	int s=0;
	for (int i=1;i<=3;i++)
		for (int j=1;j<=3;j++){
			cin>>a[i][j];
			if (i!=3||j!=3) s=s*10+a[i][j];//最后一个数不用计算 
		}
			
	memset(step,-1,sizeof(step));
	step[s] = 0;
	belong[s] = 1;
	step[12345678]=0; belong[12345678] = 2;
	queue<int> q;
	q.push(s);
	q.push(12345678); 
	while (q.size()){
		int s=q.front();
		int cur_step = step[s];
		int cur_belong = belong[s];
		q.pop();
		int x,y;
		memset(vis,false,sizeof(vis));
		
		for (int i=3;i>=1;i--)
			for (int j=3;j>=1;j--)
				if (i!=3 || j!=3)
				{
					a[i][j] = s%10;
					s=s/10;
					vis[a[i][j]] = true;
				}
				
		for (int i=0;i<9;i++)
			if (!vis[i]) a[3][3] = i;
			
		for (int i=1;i<=3;i++)
			for (int j=1;j<=3;j++)
				if (a[i][j]==0) x=i,y=j;
				
		for (int d=0;d<4;d++){
			int xx=x+dx[d];
			int yy=y+dy[d];//(x,y) 和 (xx,yy)交换
			
			if (xx>=1 && xx<=3 && yy>=1 && yy<=3){
				swap(a[x][y],a[xx][yy]);
				s=0;
				for (int i=1;i<=3;i++)
					for (int j=1;j<=3;j++)
						if (i!=3 || j!=3) s=s*10+a[i][j];
						
				if (step[s] == -1){
					step[s] = cur_step+1;//从起点来的,步数+1 
					belong[s] = cur_belong;//从终点来的 
					q.push(s);
				}
				
				else if (belong[s] != cur_belong){//相交 
						int ans = cur_step + 1 + step[s];//从起点到这个点的步数话从终点到这个点的步数相加再+1 
						cout << ans << endl;
						exit(0);
					}
				swap(a[x][y],a[xx][yy]);
			} 
		}
	}
	cout << step[12345678] << endl;
}

P1074

  • 卡时

  • 最优性剪枝

  • 改变搜索顺序
    倒着搜、横着搜......

随机化搜索

图论

概念介绍

图的详细介绍可以看 这里这里

  • 基本要素
    点+边。

  • 基本含义
    :在无向图中,顶点 \(v\) 的度是指与顶点 \(v\) 相连的边的数目。
    入度:在有向图中,以该顶点为终点的边的数目和。
    出度:在有向图中,以该顶点为起点的边的数目和。
    自环:一条边的起点和终点为同一个点。
    简单路径:不能走重复点和边的路径。
    :起点 \(=\) 终点的路径。
    简单环:简单路径和环的集合体,保证起点 \(=\) 终点且除起点(终点)外不能经过重复的点和边的路径。

图的分类

  • 根据边的属性来分类:
    有向图、无向图

  • 根据图的结构来分类:
    1.:无向无环且联通(任意两点间都可以互相到达)的图。\(n\) 个点,\(n-1\) 条边。树 + 一条边 = 章鱼图。
    2.森林:无向无环但不连通的图(有很多的树组成的图)。
    3.有向树:有向无环联通的图。
    4.外向树:所有的边都向外处指的有向树。
    5.内向树:所有的边都指向一个点的有向树。
    6.章鱼图:无向联通且只有一个环的图(把若干棵树的某一个点用环串通在一起)。\(n\) 个点,\(n\) 条边。章鱼图 - 一条边 = 树。
    7.仙人掌:无向联通且有多个环的图。

DAG:有向无环图。
二分图:把无向图的所有的点分为两个部分,第一部分的点连接的一定是第二部分的点,第二部分的点连接的一定是第一部分的点。也就是说一条边一定是连接第一部分和第二部分的点。不要求联通。数和森林就是二分图。在树中,深度为奇数的为第一部分,深度为偶数的为第二部分。存在奇环的不是二分图,没有奇环的就是二分图。同样,所有的二分图也一定没有奇环

存图

邻接矩阵

#include<bits/stdc++.h>
using namespace std;
int z[105][105];
//z[i][j]从 i 到 j 那条边的长度 
int n,m;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;//起点为s,终点为e,长度为d
		z[s][e]=d;
		//z[e][s]=d; //无向图 
	}
}

弊端:消耗空间大
优点:简单 快

边表

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int>> z[100005];
//z[i] 代表所有以 i 为起点的边
//z[i][j] 代表以 i 为起点的第 j 条边
//z[i][j].first 代表以 i 为起点的第 j 条边的终点 
//z[i][j].second 代表以 i 为起点的第 j 条边的长度 
 
void add_edge(int s,int e,int d){//添加一个从 s 出发到 e 的长度为 d 的边 
	//push_back:向 vector 的末尾加入一个元素
	z[s].push_back(make_pair(e,d)); 
}
int n,m;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;//起点为s,终点为e,长度为d
		
		add_edge(s,e,d);
		//add_edge(e,s,d);//无向图 
	}
	
	for(int i=1;i<=n;i++){//枚举从 i 号点出发的所有的边
	 	for(int j=0;j<z[i].size();j++){//z[i].size()代表从 i 出发有几条边 
	 		int e=z[i][j].first;
			int d=z[i][j].second;
			
			//代表当前的边是从 i 开始的第 j 条边,终点为 e,长度为 d 
		} 
	} 
}

弊端:慢
优点:省内存

例题 9

给你 \(n\) 个点 \(m\) 条边的无向图,每条边给定起点 \(s\) 和终点 \(e\),判断其是不是二分图。

染色法:
把一个点染色为 \(1\),然后一层一层向外染色。如果一条边连接的两个点的颜色一样,就说明不是二分图,否则就是。
BFS。

vector<int> z[maxn];

void add_edge(int s,int e)//添加一条从s出发 到e 边
{
	//push_back:向vector末尾加入一个元素 
	z[s].push_back(e);
} 

int col[maxn];
//col[i]==0 代表还没有染色
//否则代表第i个点的颜色
 
int main(){
	cin >> n >> m;
	for (int i=1;i<=m;i++)
	{
		int s,e;//起点s 终点e
		cin >> s >> e;
		
		add_edge(s,e);
		add_edge(e,s);//无向图 
	}
	
	for (int i=1;i<=n;i++)
	{
		if (col[i]!=0) continue;//如果已经染色就跳过 
		col[i] = 1;
		queue<int> q;//可以改变周围点颜色的点
		q.push(i);
		while (q.size()) {
			int i=q.front();
			q.pop();
			for (int j=0;j<z[i].size();j++) {
				int k=z[i][j];//是从i->k的一条边
				if (!col[k]) {
					col[k] = 3-col[i];
					q.push(k);	
				} else {
					if (col[k] == col[i])
					{
						cout << "No" << endl;
						exit(0);
					}
				}
			}
		} 
	}
	cout << "Yes" << endl;
}

拓扑排序

B3644

洛谷链接:B3644 【模板】拓扑排序 / 家谱树

拓扑排序:针对 DAG。
对有向图中的每一个点进行排序,使得最后的排序结果都是从左向右指的。
不断删除入度为 \(0\) 的点,然后不断把入度为 \(0\) 的点加入答案。
那如何删除一个点呢?其实不需要删除,只需要将入度减掉即可。

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int>> z[100005];
//z[i] 代表所有以 i 为起点的边
//z[i][j] 代表以 i 为起点的第 j 条边
//z[i][j].first 代表以 i 为起点的第 j 条边的终点 
//z[i][j].second 代表以 i 为起点的第 j 条边的长度 
 
void add_edge(int s,int e,int d){//添加一个从 s 出发到 e 的长度为 d 的边 
	//push_back:向 vector 的末尾加入一个元素
	z[s].push_back(make_pair(e,d)); 
}
int n,m;
int in[100005];//in[i] 代表 i 点的入度 
int result[100005];
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;//起点为s,终点为e,长度为d
		
		add_edge(s,e,d);
		in[e]++; 
	}
	
	int cnt=0;
	for(int i=1;i<=n;i++)
		if(in[i]==0)result[++cnt]=i;
		
	for(int i=1;i<=n;i++){//当前要取出拓扑排序的结果中的第 i 个点 
		int p=result[i];
		for(int j=0;j<z[p].size();j++){
			int q=z[p][j];//p->q的边
			in[q]--;
			if(in[q]==0)result[++cnt]=q; 
		} 
	}	
}

LCA

例题 10

给你一棵树和两个点 \(i\)\(j\),找到一个深度最大的公共祖先。
\(LCA(i,j)\):最近的公共祖先。

首先先算每个点的深度 depth 和每个点的父亲 f

  • 普通做法
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
const int MAXN=5e5+5;
 
int N,M,S;//N 节点数量,M 询问的次数,S 根节点的编号 
int depth[MAXN];//depth[i] 代表节点 i 的深度
int f[MAXN];//f[i] 代表节点 i 的父亲
 
std::vector<int> z[MAXN];
void add_edge(int s,int e){z[s].push_back(e);}//建边 

void dfs(int i,int j){//搜索到了节点 i,其父亲为 j 
	f[i]=j;
	depth[i]=depth[j]+1;
	for(int k=0;k<z[i].size();k++){//遍历所有与 i 相邻的点 
		int l=z[i][k];
		if(l!=j)dfs(l,i);//保证不是父节点,防止无限递归 
	}
}

int get_lca(int x,int y){//求出 x,y 的 LCA(最近公共祖先) 
	while(x!=y){
		if(depth[x]<depth[y])std::swap(x,y);//保证 x 深度较深 
		x=f[x];//往上找祖先 
	}
	return x;
}

inline int read(){//快读 
	int x=0,f=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
	while (ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

signed main(){
	N=read(),M=read(),S=read();
	for(int i=1;i<N;i++){
		int s=read();
		int e=read();
		add_edge(s,e);
		add_edge(e,s);//无向图 
	}
	
	dfs(S,0);//从根节点 S 开始 
	
	while(M--){
		int x=read();
		int y=read();
		std::cout<<get_lca(x,y)<<endl;
	}
	return 0;
}
  • 倍增算 LCA
    思想同倍增。
#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int> > z[100005];
//z[i] 代表所有以 i 为起点的边
//z[i][j] 代表以 i 为起点的第 j 条边
//z[i][j].first 代表以 i 为起点的第 j 条边的终点 
//z[i][j].second 代表以 i 为起点的第 j 条边的长度 
 
void add_edge(int s,int e,int d){//添加一个从 s 出发到 e 的长度为 d 的边 
	//push_back:向 vector 的末尾加入一个元素
	z[s].push_back(make_pair(e,d)); 
}
int n,m;
int depth[1005];//代表每个节点的深度 
int f[1005][20];//f[i][j]代表从 i 向上走 2^j 步会走到哪里 

void dfs(int i,int j){//当前要搜索 i 点的信息,是从父亲 j 走到了 i 
	f[i]=j;//i 的父亲为 j
	
	for(int k=1;k<20;k++)
		f[i][k]=f[f[i][k-1]][k-1];
	 
	depth[i]=depth[j]+1;//自己的深度=父亲的深度+1
	for(int k=0;k<z[i].size();k++){
		int l=z[i][k].first;//这条边是从 i->l 的边
		if(l!=j)dfs(l,i);//保证连接的不是父节点 
	}
}

int get_lca(int p1,int p2){//得到 p1 和 p2 的 lca 
	//把 p1,p2 两个点的深度调整成一样的 
	if(depth[p1]<depth[p2])swap(p1,p2);
	for(int i=19;i>=0;i--)
		if(depth[f[p1][i]]>=depth[p2])p1=f[p1][i];
	if(p1==p2)return p1;	
	//跳到lca
	for(int i=19;i>=0;i--)
		if(f[p1][i]!=f[p2][i])p1=f[p1][i],p2=f[p2][i];
	return f[p1][0];
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;//起点为s,终点为e,长度为d
		
		add_edge(s,e,d);
		add_edge(e,s,d);
	}
	dfs(1,0); 
}
  • 带边权的求最大值
vector< pair<int,int> > z[maxn];
void add_edge(int s,int e,int d)//添加一条从s出发 到e 边
{
	//push_back:向vector末尾加入一个元素 
	z[s].push_back(make_pair(e,d));
} 
int depth[maxn];//代表每个节点的深度
int f[maxn][20];//f[i][j]代表从i向上走2^j步 会走到哪里 
int fe[maxn][20];//fe[i][j]代表从i向上走2^j步经过的所有边的最大值 
void dfs(int i,int j, int d)
//当前要搜索i点的信息 是从父亲j走到了i 从父亲j到i这条边的长度为d 
{
	f[i][0]=j;
	for (int k=1;k<20;k++)
		f[i][k] = f[ f[i][k-1] ][k-1];
	fe[i][0] = d;
	for (int k=1;k<20;k++)
		fe[i][k] = max(fe[i][k-1], fe[ f[i][k-1] ][k-1]);
	detph[i]=depth[j]+1;
	for (int k=0;k<z[i].size();k++)
	{
		int l=z[i][k].first;//这条边是从i->l的边
		int d=z[i][k].second;
		if (l!=j) dfs(l,i,d); 
	}
} 

int get_lca(int p1,int p2)//求p1->p2路径上的最大值 
//logn
{
	int ans=0; 
	//把p1,p2两个点深度调整成一样的
	if (depth[p1]<depth[p2]) swap(p1,p2);
	for (int i=19;i>=0;i--)
		if (depth[f[p1][i]] >= depth[p2]) {
			ans=max(ans,fe[p1][i]);
			p1=f[p1][i];
		}
	if (p1==p2) return ans;
	//跳到lca
	for (int i=19;i>=0;i--)
		if (f[p1][i] != f[p2][i]){
			ans=max(ans,max(fe[p1][i],fe[p2][i]));
			p1=f[p1][i],p2=f[p2][i];
		}
	ans=max(ans,max(fe[p1][0],fe[p2][0]));
	return ans;
} 

int main()
{
	cin >> n >> m;
	for (int i=1;i<=m;i++)
	{
		int s,e,d;//起点s 终点e
		cin >> s >> e >> d;
		
		add_edge(s,e,d);
		add_edge(e,s,d); 
	}
	dfs(1,0);
}

Day 4

最短路问题

基础定义

  • 单源最短路
    求一个起点到其他点的最短路,起点是固定的。

  • 多源最短路
    求多个起点到其他点的最短路,起点不固定。

  • 最短路的三角不等式
    \(dist_{i,j} \leq dist_{i,k} + dist_{k,j}\)

多源最短路算法

Floyd

\(dist_{i,j,k}\) 代表从 \(j\) 走到 \(k\) 且走过的点的编号都 \(\leq i\) 的最短路。
最后求出 \(dist_{n,j,k}\) 来作为我们的答案。

如果 \(j\)\(k\) 有边,\(dist_{0,j,k}=d_{j,k}\)
如果 \(j\)\(k\) 无边,\(dist_{0,j,k}=∞\)

\(dist_{i,j,k}=min(dist_{i-1,j,k},dist_{i-1,j,i}+dist_{i-1,i,k})\)

  • 基础做法
//O(n^3) n<=250
#include<bits/stdc++.h>
using namespace std;

int dist[1005][1005][1005];
//dist[i][j][k] 代表从 j 走到 k 使得中间经过的节点编号 <=i 的情况下的最短路
const int INF=0x3f3f3f3f;
int n,m;//n 点 m 边 

signed main(){
	memset(dist,0x3f,sizeof(dist));//把数组每一个元素赋值为INF 
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int s,e,d;
		cin>>s>>e>>d;//一条从 s 到 e 长度为 d 的边
		dist[0][s][e]=min(dist[0][s][e],d); 
	} 
	
	for(int i=1;i<=n;i++)
		dist[0][i][i]=0;
		
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=n;k++)
				dist[i][j][k]=min(dist[i-1][j][k],dist[i-1][j][i]+dist[i-1][i][k]);
	
	int T;
	cin>>T;
	while(T--){
		int i,j;
		cin>>i>>j;
		cout<<dist[n][i][j]<<'\n';
	}		
	return 0;
} 
  • 压维
    把所有 dist 的第一个维度删掉。
//O(n^3) n<=250
#include<bits/stdc++.h>
using namespace std;

int dist[1005][1005];
//dist[i][j][k] 代表从 j 走到 k 使得中间经过的节点编号 <=i 的情况下的最短路
const int INF=0x3f3f3f3f;
int n,m;//n 点 m 边 

signed main(){
	memset(dist,0x3f,sizeof(dist));//把数组每一个元素赋值为INF 
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int s,e,d;
		cin>>s>>e>>d;//一条从 s 到 e 长度为 d 的边
		dist[s][e]=min(dist[s][e],d); 
	} 
	
	for(int i=1;i<=n;i++)
		dist[i][i]=0;
		
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=n;k++)
				dist[j][k]=min(dist[j][k],dist[j][i]+dist[i][k]);
	
	int T;
	cin>>T;
	while(T--){
		int i,j;
		cin>>i>>j;
		cout<<dist[i][j]<<'\n';
	}		
	
	return 0;	
} 

B3647

洛谷链接:B3647 【模板】Floyd 算法

注意是无向图,所以存图时要存两次。这里就不放代码了。

单源最短路算法

Dijkstra

限制:边的长度必须都 \(\geq 0\)

  1. 选一个最短路已经求好的点,选的点一定是当前 dist 最小的点。
  2. 进行松弛操作(用自己的最短路去更新自己周围的最短路)。
#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int>> z[100005];
int dist[100005];//dist[i] 代表从起点到 i 的最短路
bool vis[i];//vis[i]代表 i 的最短路有没有被求出来 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m;

void Dijkstra(int s){//以 s 作为起点算最短路 
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;
	for(int i=1;i<=n;i++){
		//选一个 dist 最小的点
		int p=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&(p==0||dist[j]<dist[p]))p=j;
		vis[p]=true;
		 
		//用这个点去进行松弛操作 
		for(int j=0;j<z[p].size();j++){
			int q=z[p][j].first;
			int d=z[p][j].second;///这是一条从 p 到 q 长度为 d 的边
			dist[q]=min(dist[q],dist[p]+d);
		}
	} 
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
	}
	
	Dijkstra(1);
	
	int T;
	cin>>T;
	while(T--){
		int x;
		cin>>x;
		cout<<dist[x]<<'\n';
	}
	return 0;
}

Dijkstra 优化

用堆。

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int>> z[100005];
int dist[100005];//dist[i] 代表从起点到 i 的最短路
bool vis[i];//vis[i]代表 i 的最短路有没有被求出来 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m;

void Dijkstra(int s){//以 s 作为起点算最短路 
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;
	priority_queue<pair<int,int> > heap;
	//first 用来存距离的相反数
	//second 用来存点的编号
	
	for(int i=1;i<=n;i++)
		heap.push(make_pair(-dist[i],i)); 
	for(int i=1;i<=n;i++){
		//选一个 dist 最小的点
		while(vis[heap.top().second])
			heap.pop();
		int p=heap.top().second;
		heap.pop();
		vis[p]=true;
		 
		//用这个点去进行松弛操作 
		for(int j=0;j<z[p].size();j++){
			int q=z[p][j].first;
			int d=z[p][j].second;///这是一条从 p 到 q 长度为 d 的边
			if(dist[q]>dist[p]+d){
				dist[q]=dist[p+d];
				heap.push(make_pair(-dist[q],q)); 
			}
		}
	} 
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
	}
	
	Dijkstra(1);
	
	int T;
	cin>>T;
	while(T--){
		int x;
		cin>>x;
		cout<<dist[x]<<'\n';
	}
	return 0;
}

P3371

洛谷链接:P3371 【模板】单源最短路径(弱化版)
#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int> > z[500005];
int dist[500005];//dist[i] 代表从起点到 i 的最短路
bool vis[500005];//vis[i]代表 i 在不在队列里 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m,x;

void SPFA(int s){//以 s 作为起点算最短路 
	for(int i=1;i<=n;i++)dist[i]=2147483647;
	dist[s]=0;
	queue<int> q;//用来存可能改变其他点最短路的点
	q.push(s);
	vis[s]=true;
	while(!q.empty()){
		int p=q.front();
		q.pop();
		vis[p]=false;
		
		for(int i=0;i<z[p].size();i++){
			int e=z[p][i].first;
			int d=z[p][i].second;
			if(dist[e]>dist[p]+d){
				dist[e]=dist[p]+d;
				if(!vis[e])q.push(e),vis[e]=true;
			}
		}
	} 
}

signed main(){
	cin>>n>>m>>x;
	for(int i=1;i<=m;i++){
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
	}
	
	SPFA(x);
	
	for(int i=1;i<=n;i++){
		cout<<dist[i]<<' ';
	}
	return 0;
}

P4779

洛谷链接:P4779 【模板】单源最短路径(标准版)

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int > > z[500005];
int dist[500005];//dist[i] 代表从起点到 i 的最短路
bool vis[500005];//vis[i]代表 i 的最短路有没有被求出来 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m,x;

void Dijkstra(int s){//以 s 作为起点算最短路 
	for(int i=1;i<=n;i++)dist[i]=2147483647;
	dist[s]=0;
	priority_queue<pair<int,int> > heap;
	//first 用来存距离的相反数
	//second 用来存点的编号
	
	for(int i=1;i<=n;i++)
		heap.push(make_pair(-dist[i],i)); 
		
	for(int i=1;i<=n;i++){
		//选一个 dist 最小的点
		while(vis[heap.top().second])
			heap.pop();
			
		int p=heap.top().second;
		heap.pop();
		vis[p]=true;
		 
		//用这个点去进行松弛操作 
		for(int j=0;j<z[p].size();j++){
			int q=z[p][j].first;
			int d=z[p][j].second;///这是一条从 p 到 q 长度为 d 的边
			if(dist[q]>dist[p]+d){
				dist[q]=dist[p]+d;
				heap.push(make_pair(-dist[q],q)); 
			}
		}
	} 
}

signed main(){
	int x; 
	cin>>n>>m>>x;
	for(int i=1;i<=m;i++){
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
	}
	
	Dijkstra(x);
	
	for(int i=1;i<=n;i++){
		cout<<dist[i]<<' ';
	}
	return 0;
}

Bellman_ford

可以针对负数边权,但是更慢。

#include<bits/stdc++.h>
using namespace std;
int s[100005],d[1000005],e[1000005];
int n,m;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>s[i]>>e[i]>>d[i];
		
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	
	for(int i=1;i<n;i++)
		for(int j=1;j<=m;j++)
			dist[e[j]]=min(dist[e[j]],dist[s[j]]+d[j]);
}

SPFA

维护能改变其他点最短路的点的队列,直至队列为空。

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int> > z[500005];
int dist[500005];//dist[i] 代表从起点到 i 的最短路
bool vis[500005];//vis[i]代表 i 在不在队列里 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m,x;

void SPFA(int s){//以 s 作为起点算最短路 
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;
	queue<int> q;//用来存可能改变其他点最短路的点
	q.push(s);
	vis[s]=true;
	while(!q.empty()){
		int p=q.front();
		q.pop();
		vis[p]=false;
		
		for(int i=0;i<z[p].size();i++){
			int e=z[p][i].first;
			int d=z[p][i].second;
			if(dist[e]>dist[p]+d){
				dist[e]=dist[p]+d;
				if(!vis[e])q.push(e),vis[e]=true;
			}
		}
	} 
}

signed main(){
	cin>>n>>m>>x;
	for(int i=1;i<=m;i++){
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
	}
	
	SPFA(x);
	
	for(int i=1;i<=n;i++){
		cout<<dist[i]<<' ';
	}
	return 0;
}

P5651

洛谷链接:P5651 基础最短路练习题

#include<bits/stdc++.h>
using namespace std;
 
vector<pair<int,int> > z[500005];
int dist[500005];//dist[i] 代表从起点到 i 的最短路
bool vis[500005];//vis[i]代表 i 在不在队列里 
 
void add_edge(int s,int e,int d){
	z[s].push_back(make_pair(e,d)); 
}
int n,m,q;

void SPFA(int s){//以 s 作为起点算最短路 
	queue<int> q;
	q.push(s);
	memset(dist,0,sizeof dist);
	memset(vis,0,sizeof vis);
	while(q.size()){
		int p=q.front();
		q.pop();
		if(vis[p]==1)continue;
		vis[p]=true;
		
		for(int i=0;i<z[p].size();i++){
			int e=z[p][i].first;
			int d=z[p][i].second;
			if(vis[e]==0){
				dist[e]=(dist[p] xor d);
				q.push(e);
			}
		}
	} 
}

signed main(){
	cin>>n>>m>>q;
	for(int i=1;i<=m;i++)
	{
		int s,e,d;
		cin>>s>>e>>d;
		add_edge(s,e,d);
		add_edge(e,s,d);
	}
	SPFA(1);
	for(int i=1;i<=q;i++)
	{
		int x,y;
		cin>>x>>y;
		cout<<(dist[y]^dist[x])<<endl;
	}
	return 0;
}

差分约束

给你 \(n\) 个变量,\(m\) 个不等式,每个不等式都会告诉你 \(x_i-x_j \geq y\) 在满足所有不等式的前提下, \(x_n-x_1\) 的最大值是多少。

把不等式转化为一条一条的边,然后求最短路。

求最小值就是求最长路。

生成树

  • 定义
    从一张 \(m\) 条边的图中找 \(n-1\) 条边,使得找出来的边和已有的点构成一棵树,组成的图就叫做原图的生成树。一个生成树的大小是选出来的所有边的边权之和。大小最小的生成树被称为 最小生成树
    最小生成树算法戳这里

Kruscal

把所有的边按照他们的边权进行排序。
怎么知道有没有环呢?并查集,查看他们最后指向的点是否一样。

  • 基础做法:O(nm)
//O(nm) 
#include<bits/stdc++.h>
using namespace std;

int to[MAXN];//to[i] 表示 i 点在并查集里面的箭头指向谁

int go(int p){//看一下点 p 沿着并查集箭头最后会走到哪里 
	if(to[p]==p)return p;//指向自己
	else return go(to[p]);//递归调用 
} 

struct edge{
	int s,e,d;
}ed[MAXN];//ed[i] 代表第 i 条边是在 s 与 e 之间的长度为 d 的边
 
int n,m;

bool cmp(edge a,edge b){
	return a.d<b.d;
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>ed[i].s>>ed[i].e>>ed[i].d;
		
	sort(ed+1,ed+m+1,cmp);//所有边按照边权进行排序
	
	for(int i=1;i<=n;i++)
		to[i]=i;//初始化并查集
		
	int ans=0;//生成树大小
	int cnt=0;//边的数量 
	for(int i=1;i<=m;i++){
		if(cnt==n-1)break; 
		int p1=ed[i].s,p2=ed[i].e,d=ed[i].d;
		if(go(p1)!=go(p2)){//不在同一个连通块 
			ans+=d;
			to[go(p1)]=go(p2);
			++cnt; 
		}
	}
	
	cout<<ans<<endl;		
} 
  • 并查集的路径压缩:O(mlogm)
//O(mlogm) sort最慢 
#include<bits/stdc++.h>
using namespace std;

int to[MAXN];//to[i] 表示 i 点在并查集里面的箭头指向谁

int go(int p){//看一下点 p 沿着并查集箭头最后会走到哪里 
	//O(1) 
	if(to[p]==p)return p;//指向自己
	/*else{
		int q=go(to[p]);
		to[p]=q;
		return q;
	}*/
	else return to[p]=go(to[p]);
} 

struct edge{
	int s,e,d;
}ed[MAXN];//ed[i] 代表第 i 条边是在 s 与 e 之间的长度为 d 的边
 
int n,m;

bool cmp(edge a,edge b){
	return a.d<b.d;
}

signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>ed[i].s>>ed[i].e>>ed[i].d;
		
	sort(ed+1,ed+m+1,cmp);//所有边按照边权进行排序
	
	for(int i=1;i<=n;i++)
		to[i]=i;//初始化并查集
		
	int ans=0;//生成树大小
	int cnt=0;//边的数量
	for(int i=1;i<=m;i++){//O(m)
		if(cnt==n-1)break;
		int p1=ed[i].s,p2=ed[i].e,d=ed[i].d;
		if(go(p1)!=go(p2)){//不在同一个连通块
			ans+=d;
			to[go(p1)]=go(p2);
			++cnt;
		}
	}

	cout<<ans<<endl;
}

P3366

P3366 【模板】最小生成树
代码同上。

例题 11

给定 \(n\) 个点和 \(m\) 条边,每条边是黑色或白色。问是否存在一棵生成树,使得白色边的数量是斐波那契数列中的某一项。

HDU 4736

求最小生成树,最多有 \(x\) 条白边。
求最大生成树,最少有 \(y\) 条白边。
只要让斐波那契数列中的某个数 \(f_i\),使得 \(y \leq f_i \leq x\) 即可。

次小生成树

第二小的生成树。
删掉一条边,再加上一条边,使得差值尽量小,并且要是一个树。
如果一条边在最小生成树上,我们就叫他树边,如果不在最小生成树上就叫他非树边。
删掉一条树边,加上一条非树边。
倍增 LCA 询问环上最大的值(章鱼图)。

P4180

P1967

最小值最大:最大生成树
用次小生成树来改改。

P3280

约束条件没有用,因为只输出卖的黄金的数量,每次订单直接把 \(x\) 的黄金全部买入。
限制为 \(y\),那么带到下一个点的黄金数量等于限制量和现有黄金数量的最小值。
总结:买点全买,卖点全卖

例题 12

给你 \(n\) 个点的无向联通带权图,其中有 \(k\) 个充电中心。给你 \(t\) 次询问,每次让机器人从 \(p\) 走到 \(q\)。问每次机器人电池容量最小是多少。其中,\(n,m,k,t \leq 10^5\) 且保证起点和终点都能充电。

最大值最小:最小生成树

CF 1253 F

例题 13

给你 \(n\) 个数,有 \(m\) 次操作,每次给定 \(l,r,v\),把 \([l,r]\) 全部赋值为 \(v\),最后输出每个数的值。\(n,m \leq 10^6\)

倒着赋值。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e7+5; 

int n,m;
int to[MAXN];
int a[MAXN];

struct node{
	int l,r,v;
}q[MAXN];

int go(int p){//看一下点 p 沿着并查集箭头最后会走到哪里 
	//O(1) 
	if(to[p]==p)return p;//指向自己
	else return to[p]=go(to[p]);
} 

signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>q[i].l>>q[i].r>>q[i].v;
		
	for(int i=1;i<=n;i++)
		to[i]=i;//初始化 
		
	for(int i=m;i>=1;i--){
		int l=q[i].l;
		int r=q[i].r;
		int v=q[i].v;//当前要对区间 l~r 染色成 v
		for(int j=l;j<=r;j++){
			j=go(j);
			if(j<=r){
				to[go(j)]=go(r);
				if(!a[j])a[j]=v;
			}
		} 
	}
	
	for(int i=1;i<=n;i++)
		cout<<a[i]<<' ';
		
	return 0; 
}

Day 5

二分图匹配问题

在二分图中找到尽量多的点,使得能够互相匹配,但是每个点只能用一次。
image

如图,只能匹配三条边,因为每个点只能匹配一次,所以 \(1-B\)\(3-B\) 只能用一个。

匈牙利算法

让已经匹配好的点断开其匹配,去找其他点。如果没有其他可以匹配的点,那么就会拒绝匹配,一路返回(有点像 DFS)。

  • 基础做法
//O(n^2 m)=O(n^3) 
#include<bits/stdc++.h>
using namespace std;
const int MAXN=500+5; 

int ans;
int n,m,k;//左边有 n 个点,右边有 m 个点,中间有 k 条边 
bool z[MAXN][MAXN];//z[i][j]代表左边第 i 个点和右边第 j 个点能不能匹配 
bool vis[MAXN];//vis[i] 代表右边第 i 个点在这一轮中有没有被请求过匹配
int result[MAXN];
//result[i] 代表右边第 i 个点和左边第 result[i] 个点匹配 

bool dfs(int i){//让左边第 i 个点尝试匹配,返回是否成功 
	for(int j=1;j<=m;j++)//让左边的第 i 个点和右边第 j 个点尝试匹配 
		if(z[i][j]&&!vis[j]){
			vis[j]=true;
			if(!result[j]||dfs(result[j])){
				result[j]=i;
				return true;
			}
		} 
	return false;
}	

signed main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++){
		int p1,p2;
		cin>>p1>>p2;
		z[p1][p2]=true; 
	}
	
	for(int i=1;i<=n;i++){
		memset(vis,false,sizeof(vis));
		if(dfs(i))ans++;//左边第 i 个点如果匹配成功,那么答案加一 
	} 
	
	cout<<ans<<endl;
} 
  • 边表优化
//O(n*边数) 
#include<bits/stdc++.h>
using namespace std;
const int MAXN=500+5; 

int ans;
int n,m,k;//左边有 n 个点,右边有 m 个点,中间有 k 条边 
vector<int> z[MAXN]//z[i][j]代表左边第 i 个点和右边第 j 个点能不能匹配 
bool vis[MAXN];//vis[i] 代表右边第 i 个点在这一轮中有没有被请求过匹配
int result[MAXN];
//result[i] 代表右边第 i 个点和左边第 result[i] 个点匹配 

void add_edge(int s,int e){
	z[s].push_back(e); 
}

bool dfs(int i){//让左边第 i 个点尝试匹配,返回是否成功 
	for(int k=0;k<z[i].size();k++){
		int j=z[i][k];
		if(!vis[j]){
			vis[j]=true;
			if(!result[j]||dfs(result[j])){
				result[j]=i;
				return true;
			}
		}
	}
		 
	return false;
}	

signed main(){
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++){
		int p1,p2;
		cin>>p1>>p2;
		add_edge(p1,p2);
	}
	
	for(int i=1;i<=n;i++){
		memset(vis,false,sizeof(vis));
		if(dfs(i))ans++;//左边第 i 个点如果匹配成功,那么答案加一 
	} 
	
	cout<<ans<<endl;
} 

P3386 B3605

洛谷链接:
P3386 【模板】二分图最大匹配

B3605 [图论与代数结构 401] 二分图匹配

超水的两道绿题,代码同上。

例题 14

给你一个 \(m×n\) 的方格图,每个格子的颜色为黑色或白色。有一个大小为 \(1×2\) 的长方形方块,一个格子为黑色,一个格子为白色,可以任意旋转。问该方格图中可以放几个这样的长方形方块,要求黑色对黑色,白色对白色。

二分图匹配。
考虑如何建图。
二分图左半部分的点为黑色格子;
二分图有半部分的点为白色格子;
如果一个黑色格子和一个白色格子相邻,就在这两个点之间建边。
最后跑一遍二分图最大匹配即可。

这里只放建图的代码。

struct node{
	int x,y;//横纵坐标
	int num;//在各自部分中的编号 
}p1[MAXN];//黑色
struct node{
	int x,y;
	int num; 
}p2[MAXN];//白色 

int n,m;
int num1;//左半部分点的数量
int num2;//右半部分点的数量 
int col; 

bool z[MAXN][MAXN];//z[i][j]代表左边第 i 个点和右边第 j 个点能不能匹配 

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			cin>>col;
			if(col==1){//黑色 
				num1++;
				p1[num].x=i;
				p1[num].y=j;
			}
			if(col==1){//白色 
				num2++;
				p2[num].x=i;
				p2[num].y=j;
			}
		}
	
	for(int i=1;i<=num1;i++)
		for(int j=1;j<=num2;j++){
			int x1=p1[i].x;
			int y1=p1[i].y;//黑色格子 
			int x2=p2[j].x;
			int y2=p2[j].y;//白色格子
			
			if(abs(x1-x2)==1||abs(y1-y2)==1)
				z[i][j]=true;//建边 
		} 
}

例题 15

给你一个 \(n×m\) 的方格有若干个障碍物和若干个人,人能看见上下左右没有障碍物遮挡的范围(类似象棋中的車),问你最多能放几个人。

二分图里左边的点是所有的小行(被障碍物间隔开),右边的点是所有的小列,每一小行和每一小列如果有交点就建边,跑最大匹配。

HDU 5093

强连通分量

  • 定义
    在有向图中,找到若干个点和若干条边,使得每个点都可以互相走到,构成的图就叫做强连通分量。独立的点也可以作为一个强连通分量。每个点都一定要在一个强连通分量里。

  • 问题
    给你一个有向图,找到所有的强连通分量。

Tarjan

找能组成环的非树边。

什么样的非树边能产生环?
如果一条非树边连向自己的祖先,那么就称为 回边,能形成环。
如果没连向自己的祖先就叫做 横叉边,不能组成环。

如果两条横叉边能组成环,那是因为 DFS 改变了树的形态,所以那不是横叉边,而是一条回边。

横叉边虽然不能组成环,但是可以扩大环的范围,但是要求连过去的边往上走的最上面的那个点必须是横叉边开始的点的 \(k\) 的祖先。

image

int low[MAXN];
//low[i] 代表
//从 i 点出发,沿着 回边,树边 or 能扩大环的横叉边 走
//能够走到的所有点中 dfn 最小的点(深度较小)
low[i]=j;

怎么判断一条边是回边还是横叉边以及横叉边的哪种边呢?

stack<int> s;//栈用来储存被 DFS 过,但还没有求出强连通分量的点 
bool instack[MAXN];//instack[i] 代表 i 是否在栈里

image

如果走到的点还在栈里面,就说明是回边或是可以扩大范围的横叉边。

B3609

B3609 [图论与代数结构 701] 强连通分量
模板题,注意恶心的输出。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+5;

vector<int> z[MAXN];
void add_edge(int s,int e){
	z[s].push_back(e); 
}

int num;//当前已经 DFS 了 num 个点 
int dfn[MAXN];//dfn[i] 第 i 个点是第几个被 DFS 到的
int low[MAXN];
//low[i] 代表
//从 i 点出发,沿着 回边,树边 or 能扩大环的横叉边 走
//能够走到的所有点中 dfn 最小的点(深度较小) 
stack<int> s;//栈用来储存被 DFS 过,但还没有求出强连通分量的点 
bool instack[MAXN];//instack[i] 代表 i 是否在栈里面 
int cnt;//有几个强连通分量
int belong[MAXN];//belong[i] 表示 i 属于哪一个强连通分量 

void dfs(int i){//当前搜索到了 i 点
	num++;
	dfn[i]=low[i]=num;
	s.push(i);
	instack[i]=true; 
	for(int k=0;k<z[i].size();k++){
		int j=z[i][k];
		if(!dfn[j]){//这是一条树边
			dfs(j);
			low[i]=min(low[i],low[j]);
		}
		else{//非树边
			//这是一条回边 or 能扩大环的横叉边
			if(instack[j])low[i]=min(low[i],dfn[j]);
			//if(instack[j])low[i]=min(low[i],low[j]); 
			//两种写法都可以 
		}
	} 
	if(dfn[i]==low[i]){
		cnt++;//多了一个强连通分量
		while(s.top()!=i){
			belong[s.top()]=cnt;
			instack[s.top()]=false;
			s.pop();
		} 
		s.pop();
		instack[i]=false;
		belong[i]=cnt;
	} 
} 

int n,m;
signed main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int p1,p2;
		cin>>p1>>p2;
		add_edge(p1,p2);
	}
	
	for(int i=1;i<=n;i++)
		if(!dfn[i])dfs(i);
	
	cout<<cnt<<endl;
	for(int i=1;i<=cnt;i++){
		int now;
		for(int j=1;j<=n;j++)
			if(belong[j]!=-1){
				now=belong[j];
				break;
			}
		for(int j=1;j<=n;j++){
			if(belong[j]==now){
				cout<<j<<' ';
				belong[j]=-1;
			}
		}
		cout<<endl;
	}	
	
	return 0;
} 

DP(动态规划)

三个要素

要求的东西被称为 状态(基础)。

描述状态与状态之间的关系的式子叫做 转移方程

描述特殊情况下状态的值的东西叫做 初始化条件 \(or\) 边界条件

三种写法

斐波那契数列为例。

  • 用别的状态的值来求自己状态的值

    要求 \(f_i\) 的值的时候,用别的状态的值去求 \(f_i\) 的值。

    用别人的值来求自己的值。

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

int n,f[MAXN];

signed main(){
	cin>>n;
	f[0]=0;
	f[1]=1;//初始化条件
	for(int i=2;i<=n;i++)
		f[i]=f[i-1]+f[i-2];//转移方程
	cout<<f[n]<<endl;

	return 0;
}
  • 用已经求完的状态的值去更新其他状态

    枚举到 \(f_i\) 的时候,更新 \(f_i\) 可能影响到的答案的值。

    用自己的值去求别人的值。

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

int n,f[MAXN];

signed main(){
	cin>>n;
	f[0]=0;
	f[1]=1;//初始化条件
	for(int i=0;i<=n;i++){
		f[i+1]+=f[i];
		f[i+2]+=f[i];//转移方程没有改变
	}
	cout<<f[n]<<endl;

	return 0;
}
  • 记忆化搜索

    搜过的东西不再搜,每个东西只被搜一次。

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

int n;
int f[MAXN];//f[i] 代表斐波那契数列第 i 项的值
bool g[MAXN];//g[i] 代表斐波那契数列第 i 项是否被计算过

int dfs(int n){//求斐波那契数列的第 n 项的值
	if(n==0)return 0;
	if(n==1)return 1;//初始化条件
	if(g[n])return f[n];//如果计算过这个数的值,就直接返回计算过的值
	g[n]=true;
	f[n]=dfs(n-1)+dfs(n-2);//转移方程
	return f[n];
}

signed main(){
	cin>>n;
	cout<<dfs(n)<<endl;
	return 0;
}

状态定义方法

例题 16

\((1,1)\) 走到 \((n,m)\),只能向下或向右走,使得走过的点的点权之和最大。

所有发生变化的量把他作为转移方程的一部分

  • 方法一:用别人求自己
#include<bis/stdc++.h>
using namespace std;

int f[MAXN][MAXN];//f[i][j] 代表走到 (i,j) 能获得的最大权值和
int n,m;
int a[MAXN][MAXN];

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

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

	cout<<f[n][m]<<endl;
}
  • 方法二:用自己求别人
#include<bis/stdc++.h>
using namespace std;

int f[MAXN][MAXN];//f[i][j] 代表走到 (i,j) 能获得的最大权值和
int n,m;
int a[MAXN][MAXN];

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

	f[1][1]=a[1][1];
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++){
			f[i+1][j]=max(f[i+1][j],f[i][j]+a[i+1][j]);
			f[i][j+1]=max(f[i][j+1],f[i][j]+a[i][j+1]);
		}

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

P1216

原题链接:P1216 [USACO1.5] [IOI1994]数字三角形 Number Triangles

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

int n;
int a[MAXN][MAXN];

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			cin>>a[i][j];
	for(int i=n-1;i>=1;i--)//从底层倒着枚举
 		for(int j=1;j<=i;j++){
 			a[i][j]+=max(a[i+1][j],a[i+1][j+1]);//选取两个分支中答案大的那个 
		}

    cout<<a[1][1]<<endl;//输出三角形的顶部
    return 0;
 }
  • 进阶

将题面中的 “数字的和最大” 改成 “数字的和 \(\mod 100\) 最大”。

小技巧

当发现 DP 做不出来时,就增加维度,然后让等式的右边为 truefalse

此题我们定义状态 \(f_{i,j,k}\) 代表走到 \((i,j)\) 这个点权值和 \(\mod 100 = k\) 这件事可不可能。

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

int n;
int a[MAXN][MAXN];
bool f[MAXN][MAXN][MAXN];//f[i][j][k] 代表走到 (i,j) 这个点时权值和 %100 = k 这件事是否可能

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

	//改成从下往上走
	for(int i=1;i<=n;i++)
		f[n][i][a[n][i]%100]=true;

	//用自己求别人
	for(int i=n;i>=2;i--)
		for(int j=1;j<=i;j++)
			for(int k=0;k<100;k++)
				if(f[i][j][k]){//当前状态是可能的
					f[i-1][j-1][(k+a[i-1][j-1])%100]=true;
					f[i-1][j][(k+a[i-1][j])%100]=true;
				}

    int ans=-1;
    for(int k=0;k<100;k++)
		if(f[1][1][i])ans=i;

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

最长上升子序列 LIS

\(f_{选到哪里了}=\)选了几个数
\(f_i\) 代表最后一个选了 \(a_i\) 的情况下最多能选几个数。

B3637

洛谷链接:B3637 最长上升子序列

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

int n;
int a[MAXN];
int f[MAXN];//以 a[i] 作为最后一个数的时候最多选几个数

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	for(int i=1;i<=n;i++){//要求 f[i]
		for(int j=1;j<i;j++)//倒数第二个数是 f[j]
			if(a[j]<a[i])f[i]=max(f[i],f[j]);
		f[i]++; 
	}
	
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i]);
	cout<<ans<<endl;
} 
  • 进阶:求具体方案
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

int n;
int a[MAXN];
int f[MAXN];//以 a[i] 作为最后一个数的时候最多选几个数
int g[MAXN];//g[i] 代表状态 f[i] 是由状态 f[g[i]] 转移过来的 

void print(int p){//输出当前最后一个数是 a[p] 那组解
 	if(p==0)return;
	print(g[p]);
	cout<<a[p]<<' '; 
}

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	
	for(int i=1;i<=n;i++){//要求 f[i]
		for(int j=1;j<i;j++)//倒数第二个数是 f[j]
			if(a[j]<a[i])
				if(f[j]>f[i]){
					f[i]=f[j];
					g[i]=j;
				}
		f[i]++; 
	}
	
	int p=1;
	for(int i=2;i<=n;i++)
		if(f[i]>f[p])p=i;
	cout<<f[p]<<endl;
	print(p);
} 
  • 在进阶:求最优解数量
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

int n;
int a[MAXN];
int f[MAXN];//以 a[i] 作为最后一个数的时候最多选几个数
int g[MAXN];//g[i] 代表状态 f[i] 是由状态 f[g[i]] 转移过来的 
int h[MAXN];//h[i] 代表状态 f[i] 有多少种最优解 

void print(int p){//输出当前最后一个数是 a[p] 那组解
 	if(p==0)return;
	print(g[p]);
	cout<<a[p]<<' '; 
}

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
		
	for(int i=1;i<=n;i++)
		h[i]=1; 
	
	for(int i=1;i<=n;i++){//要求 f[i]
		for(int j=1;j<i;j++)//倒数第二个数是 f[j]
			if(a[j]<a[i]){
				if(f[j]>f[i]){
					f[i]=f[j];
					g[i]=j;
					h[i]=h[j];
				}
				else if(f[j]==f[i])h[i]+=h[j];
			}
				
		f[i]++; 
	}
	
	int p=1;
	for(int i=2;i<=n;i++)
		if(f[i]>f[p])p=i;
	cout<<f[p]<<endl;
	print(p);
	cout<<endl; 
	
	int ans=0;
	for(int i=1;i<=n;i++)
		if(f[i]==f[p])ans+=h[i];
	cout<<ans<<endl;
} 

P1541

洛谷链接:P1541 [NOIP2010 提高组] 乌龟棋
\(f_{i,c1,c2,c3,c4}\) 代表走到了 \(i\) 用了 \(c1\) 张走一步的卡片,\(c2\) 张走两步的卡片,\(c3\) 张走三步的卡片,\(c4\) 张走四步的卡片的和的最大值。

DP 优化技巧

消除冗余状态

\(i=c1+2×c2+3×c3+4×c4\),所以把第一个维度 \(i\) 删掉,无用。

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

int f[45][45][45][45];
int g[1005];
int n,m;
int a[1005],b[1005];

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	for(int i=1;i<=m;i++){
		int x;
		cin>>x;
		g[x]++;
	}
	
	f[0][0][0][0]=a[1];
	
	for(int i=0;i<=g[1];i++)
		for(int j=0;j<=g[2];j++)
			for(int k=0;k<=g[3];k++)
				for(int l=0;l<=g[4];l++){
					int r=1+i+2*j+3*k+4*l;
					if(i!=0)
						f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k][l]+a[r]);
					if(j!=0)
						f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k][l]+a[r]);
					if(k!=0)
						f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k-1][l]+a[r]);
					if(l!=0)
						f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k][l-1]+a[r]);
				}
				
	cout<<f[g[1]][g[2]][g[3]][g[4]]<<endl;
	return 0;
}

排列 DP

排列:有 \(n\) 个数不重复且不遗漏的出现就叫做排列。

\(n\) 个数的排列共有 \(n!\) 种。

  • 转移方法
    把所有数从大到小或从小到大地插入进去。

例题 17

\(n\) 个数的排列中,逆序对为偶数个的有多少个。

  • 状态定义
    \(f_{i,j}\) 代表现在已经把 \(1~i\) 插入了,此时逆序对的数量为 \(j\) 的方案数。

  • 转移方程
    \(f_{i+1,j+i-k}+=f_{i,j}\)\(k\) 为插入位置。

  • 基础做法

//O(n^4)
#include<bits/stdc++.h>
using namespace std;

int n;
int f[MAXN][MAXN];//f[i][j] 表示 1~i 已经插入,此时有 j 个逆序对的方案数

signed main(){
	cin>>n;
	f[1][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=i*(i-1)/2;j++)//插入 i+1
			for(int k=0;k<=i;k++)//i+1 要插入到第 k 个位置
				f[i+1][j+i-k]+=f[i][j];

	int ans=0; 
	for(int j=0;j<=n*(n-1)/2;j+=2)
		ans+=f[n][j];
	cout<<ans<<endl;
}
  • 优化
    我们只需要知道逆序对是奇数还是偶数,并不需要知道具体的值。
//O(n^2) 
#include<bits/stdc++.h> 
using namespace std;

int n;
int f[MAXN][MAXN];//f[i][j] 表示 1~i 已经插入,此时有 j 个逆序对的方案数
//j=0 代表有偶数个逆序对
//j=1 代表有奇数个逆序对 

signed main(){
	cin>>n;
	f[1][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=0;j<2;j++)//奇偶数 
			for(int k=0;k<=i;k++)//i+1 要插入到第 k 个位置
				f[i+1][(j+i-k)%2]+=f[i][j];

	ans+=f[n][0];
	cout<<ans<<endl;	
} 

(其实答案就是 \(n!/2\)

例题 18

给定 \(n\) 个数,定义一个序列的激动值为该序列中比前面的数都大的数的数量,问这个序列中有多少个子序列的激动值为 \(k\)

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

int f[MAXN][MAXN];//f[i][j] 代表 i~n 已经插入,激动值为 j 的方案数

signed main(){
	cin>>n>>k;
	f[n][1]=1;
	for(int i=n;i>=2;i--)
		for(int j=1;j<=n;i++){//接下来要插入 i-1 这个数 
			f[i-1][j+1]+=f[i][j];//插入到最前面
			f[i-1][j]=f[i][j]*f[i][j]*(n-i+1);//不插入到最前面 
		}
		
	cout<<f[1][k]<<endl; 
} 

P1521

洛谷链接:P1521 求逆序对

//O(n^2) 
#include<bits/stdc++.h> 
using namespace std;
const int MAXN=5e3+5;
const int mod=10000;


int n,k;
int f[MAXN][MAXN];//f[i][j] 表示 1~i 已经插入,此时有 j 个逆序对的方案数
//j=0 代表有偶数个逆序对
//j=1 代表有奇数个逆序对 
int ans;

signed main(){
	cin>>n>>k;
	f[1][0]=1; 
	f[2][1]=1;
	f[2][0]=1;
	f[0][0]=1;
	for(int i=3;i<=n;i++)
		for(int j=0;j<=k;j++)//奇偶数 
			for(int l=0;l<=i-1&&j-l>=0;l++)//i+1 要插入到第 k 个位置
				f[i][j]=(f[i-1][j-l]+f[i][j])%mod;

	ans=f[n][k];
	cout<<ans<<endl;	
} 

Day 6

背包 DP

01-背包

例题 19

\(n\) 个物品,背包的体积为 \(m\)。第 \(i\) 个物品的价值为 \(w_i\),体积为 \(v_i\)。问在不超过背包体积的前提下,价值最多是多少。

变化的东西:装到了第几个物品、当前的体积和当前的价值。

状态\(f_{i,j}\) 表示钱 \(i\) 个物品已经考虑完,此时用掉了 \(j\) 的体积所能获得的最大价值。

转移

  1. 不选。\(f_{i+1,j}=f_{i,j}\)
  2. 选。\(f_{i+1,j+v_i}=f_{i,j}+w_{i+1}\)
//o(nm)=O(物品数量*体积最大值) 
#include<bits/stdc++.h>
using namespace std;

int f[MAXN][MAXN];
//f[i][j] 代表前 i 个物品已经考虑完,用掉了 j 的体积所能获得的最大价值
int v[MAXN],w[MAXN];
int m,n;

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i];//读入体积和价值
		
	for(int i=0;i<n;i++)
		for(int j=0;j<=m;j++){//考虑第 i+1 个物品选不选 
			f[i+1][j]=max(f[i+1][j],f[i][j]);//不选
			f[i+1][j+v[i+1]]=max(f[i+1][j+v[i+1]],f[i][j]+w[i+1]);//选 
		} 
		
	int ans=0;
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	cout<<ans<<endl;//cout<<f[n][m]<<endl;
} 

无穷背包(完全背包)

例题 20

\(n\) 种物品,背包的体积为 \(m\)。第 \(i\) 种物品的价值为 \(w_i\),体积为 \(v_i\)。问在不超过背包体积的前提下,价值最多是多少。

不能按性价比去做,因为(只因)剩下可能会有一些空的位置,导致浪费。

  • 基础做法
//o(nm^2)
#include<bits/stdc++.h>
using namespace std;

int f[MAXN][MAXN];
//f[i][j] 代表前 i 个物品已经考虑完,用掉了 j 的体积所能获得的最大价值
int v[MAXN],w[MAXN];
int m,n;

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i];//读入体积和价值
		
	for(int i=0;i<=n;i++)
		for(int j=0;j<=m;j++)//要求状态f[i][j] 用别人去求自己 
			for(int k=0;k*v[i]<=j;k++)//考虑第 i 种物品选 k 个 
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); 
		
	int ans=0;
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	cout<<ans<<endl;//cout<<f[n][m]<<endl;
}
  • 优化
//o(nm)
#include<bits/stdc++.h>
using namespace std;

int f[MAXN][MAXN];
//f[i][j] 代表前 i 个物品已经考虑完,用掉了 j 的体积所能获得的最大价值
int v[MAXN],w[MAXN];
int m,n;

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i];//读入体积和价值
		
	for(int i=0;i<=n;i++)
		for(int j=0;j<=m;j++){//要求状态f[i][j] 用别人去求自己 
			f[i][j]=f[i-1][j];//上 
			if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);//左 
		}
		
	int ans=0;
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	cout<<ans<<endl;//cout<<f[n][m]<<endl;
}
  • 滚动数组
//o(nm)
#include<bits/stdc++.h>
using namespace std;

int f[MAXN][MAXN];
//f[i][j] 代表前 i 个物品已经考虑完,用掉了 j 的体积所能获得的最大价值
int v[MAXN],w[MAXN];
int m,n;

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i];//读入体积和价值
		
	for(int i=0;i<=n;i++)
		for(int j=0;j<=m;j++){//要求状态f[i][j] 用别人去求自己 
			f[i][j]=f[i-1][j];//上 
			if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);//左 
		}
		
	int ans=0;
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	cout<<ans<<endl;//cout<<f[n][m]<<endl;
}

有限背包(多重背包)

例题 21

\(n\) 种物品,背包的体积为 \(m\)。第 \(i\) 种物品的价值为 \(w_i\),体积为 \(v_i\),数量为 \(z_i\)。问在不超过背包体积的前提下,价值最多是多少。

  • 基础做法
//o(nm^2)
#include<bits/stdc++.h>
using namespace std;

int f[MAXN][MAXN];
//f[i][j] 代表前 i 个物品已经考虑完,用掉了 j 的体积所能获得的最大价值
int v[MAXN],w[MAXN],z[MAXN];
int m,n;

signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)
		cin>>v[i]>>w[i]>>z[i];//读入体积和、价值和个数 
		
	for(int i=0;i<=n;i++)
		for(int j=0;j<=m;j++)//要求状态f[i][j] 用别人去求自己 
			for(int k=0;k*v[i]<=j&&k<]z[i];k++)//考虑第 i 种物品选 k 个 
				f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]); 
		
	int ans=0;
	for(int i=0;i<=m;i++)
		ans=max(ans,f[n][i]);
	cout<<ans<<endl;//cout<<f[n][m]<<endl;
}
  • 优化

P4095

区间 DP

例题 22

给你 \(n\) 堆石子,第 \(i\) 堆石子的个数为 \(a_i\)。每次只能合并两堆相邻的石子,每次合并的代价为合并的两堆石子数量和,问将这 \(n\) 堆石子合并到一堆所需要的最小代价。

\(f_{l,r}\) 代表把 \(a_l~a_r\) 合并成一堆的最小代价。
最后求 \(f_{1,n}\)
其中 \(f_{i,i}=a_i\)

转移式子
\(f_{l,r}=min(f_{l,k}+f_{k+1,r}+sum_r-sum_{l-1},f_{l,r})\)
其中 \(l \leq k < r\)

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

int f[MAXN][MAXN];

int n;
int a[MAXN];
int sum[MAXN];

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
	}
	
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++)
		f[i][i]=0;
		
	for(int len=1;len<=n;len++)//当前要处理长度为 len 的区间 
		for(int l=1,r=len;r<=n;l++,r++)
			for(int k=l;k<r;k++)
				f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);

	cout<<f[1][n]<<endl;
} 

P1775

洛谷链接:P1775 石子合并(弱化版)
模板题,按上述代码交就行。

P1880

洛谷链接:P1880 [NOI1995] 石子合并
改成了环。

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

int f[MAXN][MAXN];

int n;
int a[MAXN];
int sum[MAXN];

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
	}
		
	for(int i=n+1;i<=2*n;i++){
		a[i]=a[i-n];
		sum[i]=sum[i-1]+a[i]; 
	} 
		
	
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=2*n;i++)
		f[i][i]=0;
		
	for(int len=2;len<=n;len++)//当前要处理长度为 len 的区间 
		for(int l=1,r=len;r<=2*n;l++,r++)
			for(int k=l;k<r;k++)
				f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
	int ans=0x3f3f3f3f;
	for(int i=1;i<=n;i++)
		ans=min(ans,f[i][i+n-1]) ; 
	cout<<ans<<endl;
	
	memset(f,-0x3f,sizeof(f));
	for(int i=1;i<=2*n;i++)
		f[i][i]=0;
	for(int len=2;len<=n;len++)//当前要处理长度为 len 的区间 
		for(int l=1,r=len;r<=2*n;l++,r++)
			for(int k=l;k<r;k++)
				f[l][r]=max(f[l][r],f[l][k]+f[k+1][r]+sum[r]-sum[l-1]);
	ans=0xc0c0c0c0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][i+n-1]) ; 
	cout<<ans<<endl;
} 

例题 23

给你一个字符串,问其中回文子序列的数量。

状态\(f_{l,r}\) 代表从第 \(l\) 个字符到第 \(r\) 个字符之间的回文子序列的数量。

区间 DP 总结

  • 状态
    \(f_{l,r}\) 代表 \(l~r\) 这个区间内的某些东西。
  • 转移方法
  1. 合并相邻的东西:
    枚举分界点 \(k\)
  2. 找子序列
    \(l,r\) 不同时选,分别去掉左右端点。
    \(l,r\) 同时选,找 \(l+1~r-1\) 的中间序列。
#include<bits/stdc++.h>
using namespace std;

char s[MAXN];
int f[MAXN][MAXN];//f[l][r] 代表s[l]~s[r]中有多少个回文子序列

signed main(){
	cin>>s+1;
	int n=strlen(s+1);
	for(int i=1;i<=n;i++)
		f[i][i]=1;
		
	for(int len=2;len<=n;len++)
		for(int l=1;r=len;r<=n;l++,r++){
			f[l][r]=f[l][r-1]+f[l+1][r]-f[l+1][r-1];//s[l]和s[r]不同时
			if(s[l]==s[r])f[l][r]+=f[l+1][r-1]+1; 
		} 
	
	cout<<f[1][n]<<endl;
} 

树形 DP

转移思路:从下至上,用儿子来算自己的信息。

状态:第一维度的 \(i\) 代表以 \(i\) 为根的子树(从 \(i\) 向下走可以走到所有的点组成的树)。

例题 24

给你一个 \(n\) 个点的树,问你这棵树有多少个点。(这不就是 \(n\) 个点吗)

状态\(f_i\) 代表以 \(i\) 为根的子树的点的数量。

初始化条件:叶子节点(所有没有儿子的节点)。

如果 \(i\) 为叶子节点,那么 \(f_i=1\)
如果 \(i\) 不为叶子节点,那么它等于自己所有的儿子的值的和再加一(自己本身)。

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

int n,s,e;
int f[MAXN];//f[i] 代表以 i 为根的子树的大小 

void dfs(int p,int f){//当前要 DP p点的值,其中 p 的父亲为 f 
	for(int i=0;i<z[p].size();i++){
		int q=z[p][i];//从 p 到 q 的边
		if(q!=f)dfs(q,p);//DP 儿子节点的值
	}
	
	f[p]=1;
	
		for(int i=0;i<z[p].size();i++){
		int q=z[p][i];//从 p 到 q 的边
		if(q!=f)f[p]+=f[q];
	}
} 

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int s,e;
		add_edge(s,e);
		add_edge(e,s);
	}
	dfs(1,0);
} 

例题 25

求树上路径总长度和。

简单到难以想象:

ans+=2*f[p]*(n-f[p]);

例题 26

求树的直径(树上最远的两个点的距离)。

\(f_{i,0}\)\(f_{i,1}\) 分别代表从 \(i\) 向下最长和次长的路径的长度。

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

int n,s,e;
int f[MAXN][2];
//f[i][0] 代表以 i 为根向下最长有多长
//f[i][1] 代表以 i 为根向下次长有多长 

void dfs(int p,int f){//当前要 DP p点的值,其中 p 的父亲为 f 
	for(int i=0;i<z[p].size();i++){
		int q=z[p][i].first;
		int d=z[p][i].second;//从 p 到 q 长度为 d 的边
		if(q!=f){
			int x=d+f[q][0];
			if(x>f[p][0]){
				f[p][1]=f[p][0];
				f[p][0]=x;
			} 
			else if(x>f[p][1])f[p][1]=x;
		}
	}
	
	f[p]=1;
	
	for(int i=0;i<z[p].size();i++){
		int q=z[p][i];//从 p 到 q 的边
		if(q!=f)f[p]+=f[q];
	}
} 

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int s,e,d;
		add_edge(s,e,d);
		add_edge(e,s,d);
	}
	dfs(1,0);
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][0]+f[i][1]);
	cout<<ans<<endl;
} 

例题 27

询问树的最大独立集。
大白话翻译:在树上选出尽可能多的点,使得点与点之间都互不相邻,问最多选多少个点。

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

int n,s,e;
int f[MAXN][2];
//f[i][0] 代表以 i 为根的子树 i 没选的情况下最多选几个点 
//f[i][1] 代表以 i 为根的子树 i 选了的情况下最多选几个点 

void dfs(int p,int f){//当前要 DP p点的值,其中 p 的父亲为 f 
	for(int i=0;i<z[p].size();i++){
		int q=z[p][i];
		if(q!=f)dfs(q,p); 
	}
	
	f[p][0]=1;
	f[p][1]=1;
	
	for(int i=0;i<z[p].size();i++){
		int q=z[p][i];//从 p 到 q 的边
		if(q!=f){
			f[p][1]+=f[q][0];
			f[p][0]+=max(f[q][1],f[q][0]);
		}
	}
} 

signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int s,e,d;
		add_edge(s,e,d);
		add_edge(e,s,d);
	}
	dfs(1,0);
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i][0]+f[i][1]);
	cout<<ans<<endl;
} 

数位 DP

例题 28

给你 \(l\)\(r\),问你 \(l~r\) 共有几个数(弱智问题++)

例题 29

\(l~r\) 中的所有的数的数位之和。

P4999

例题 30

\(l~r\) 中的所有的满足相邻两个数字之差至少为 \(2\) 的数有多少个。

P2657

例题 31

\(n×m\) 的棋盘上放若干个炮似的其互相不攻击的方案数。

Day 7

字符串

Hash

把字符串变成数。
详见 此处

注意问题:

  1. 转成数时不要太长以至于爆 long long
  2. 让 hash 碰撞的概率尽可能小

例题 32

求所有长度为 \(k\) 的子串中有多少个不一样的。

单模 hash 法

\(27\) 进制存储下来字符串。但是由于数太大了,所以我们定义一个 \(p\) 用来取模。但是有可能存在取模后数字一样的情况,为了使碰撞的概率尽可能小,所以我们要增大 \(p\) 的值。

双模 hash 法

自然溢出法(不模 hash 法)

让他自己爆掉 unsigned long long,让其自然取模。

所以应该用自然溢出法。

例题 33

找最长的回文子串,问最长的长度是多少。

Manacher

\(f_i\) 代表以 \(i\) 为中心的最长回文子串的长度。

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1.1*1e7+5;

int f[MAXN*2];//f[i] 代表以 i 为中心的最长回文子串的 右端点 - i
//abcda 1-5 中心为3 f[3]=2
int n;
char s[MAXN*2];

void Manacher(){
	f[1]=0;
	int nowmid=1;
	//nowmid 当前右端点最靠右的回文子串的中心
	int nowr=1;
	//nowr 当前右端点最靠右的回文子串的右端点
	for(int i=2;i<=n;i++){//要算 f[i[
		//抄答案
		if(i<=nowr) f[i]=min(f[2*nowmid-i],nowr-i);
		//暴力向左右扩展
		while(true){
			int pl=i-f[i]-1;
			int pr=i+f[i]+1;
			if(pl>=1&&pr<=n&&s[pl]==s[pr])f[i]++;
			else break;
		}
		//更新右端点最靠右的回文子串
		if(i+f[i]>nowr)nowmid=i,nowr=i+f[i]; 
	} 
}

signed main(){
	cin>>s+1;
	n=strlen(s+1); 
	for(int i=n;i>=1;i--)
		s[i*2]=s[i];
	for(int i=1;i<=2*n+1;i+=2)
		s[i]='^';
	n=n*2+1;
	Manacher();
	int ans=0;
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i]);
	cout<<ans<<endl;
	return 0;
} 

Z-box

LCP

最长公共前缀。

数学

模运算

#include<bits/stdc++.h>
using namespace std;
signed main(){
	//加法 
	x=(a+b)%p;
	x=(0ll+a+b+c)%p;
	x=((a+b)%p+c)%p; 
	
	//减法 
	x=((a-b)%p+p)%p;
	
	//乘法 
	x=1ll*a*b%p;
	x=1ll*a*b%p*c%p;
}

高精度

  • 正数
struct high_precision{
	int n;//n 代表这个数的位数
	int a[MAXN];//a[i] 代表这个数的第 i 位是多少
	//个位在 a[1]
	char s[MAXN];//读入的
	 
	high-precision(){
		n=1;
		memset(a,0,sizeof(a));
	}
	void read(){
		cin>>s+1;
		n=strlen(s+1);
		for(int i=1,j+n;i<=n;i++,j--)
			a[i]=s[j]-'0'; 
	}
	
	void print(){
		for(int i=n;i>=1;i--)
			cout<<a[i];
	}
};



high_precision operator + (const high_precision &a,const high_precision &b){
	high_precision ans;
	ans.n=max(a.n,b.n);
	
	for(int i=1;i<=ans.n;i++){
		ans.a[i]+=a.a[i]+b.a[i];
		ans.a[i+1]+=ans.a[i]/10;
		ans.a[i]=ans.a[i]%10;
	}
	
	if(ans.a[ans.n+1])ans.n++;
	return ans;
}



high_precision operator - (const high_precision &a,const high_precision &b){
	high_precision ans;
	ans.n=max(a.n,b.n);
	
	for(int i=1;i<=ans.n;i++){
		ans.a[i]+=a.a[i]-b.a[i];
		
		if(ans.a[i]<0){
			ans.a[i]+=10;
			ans.a[i+1]--;
		}
	}
	
	while(ans.n>1&&!ans.a[ans.n])
		ans.n--;
	return ans;
}



high_precision operator * (const high_precision &a,const high_precision &b){
	high_precision ans;
	ans.n=a.n+b.n;
	
	for(int i=1;i<=a.n;i++)
		for(int j=1;j<=b.n;j++)
			ans.a[i+j-1]+=a.a[i]*b.a[j];

	for(int i=1;i<=ans.n;i++){
		ans.a[i+1]+=ans.a[i]/10;
		ans.a[i]=c.a[i]%10;
	}
	
	while(c.n>1&&!ans.a[ans.n])
		ans.n--;

	return ans;
}


signed main(){
	high_precision a,b,n;
	a.read();
	b.read();
	n=a+b;
	n.print();
}
  • 负数
posted @ 2023-07-09 08:50  CheZiHe929  阅读(1096)  评论(4)    收藏  举报