jnu第一大混子的训练纪录2:dp与树形结构

这篇博客详细探讨了动态规划在解决背包问题中的应用,包括01背包、完全背包和多重背包的解题策略,以及如何优化空间复杂度。同时,介绍了图论的基础概念,如拓扑排序,通过实例展示了如何在有向无圈图中进行拓扑排序求解问题。
摘要由CSDN通过智能技术生成

训练纪录1去哪里了呢?我也不知道

以《挑战程序设计竞赛》一书为基础的练习纪录,各个专题都有涉及 

Part 1:动态规划


插个链接给自己复习背包九讲—01背包

例题:洛谷1048

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int main(){
	/*int M,T;
	int time[150],val[150];
	int dp[150][1500];
	cin>>T>>M;
	for(int i=1;i<=M;i++){
		cin>>time[i]>>val[i];
	}
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=M;i++){//i是物品 ,留第一行用来放0 
		for(int j=0;j<=T;j++){//j是重量(时间) 
			if(time[i]>j){//不能选 
				dp[i][j]=dp[i-1][j];//那么小于j重量的第i个物品的最大价值就是i-1个物品的价值(没选) 
			}else{
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-time[i]]+val[i]);//选与不选的价值都拿出来 
			} 
		}
	}
	cout<<dp[M][T];	*/

	int M,T;
	int time[150],val[150];
	int dp[1500];
	cin>>T>>M;
	for(int i=1;i<=M;i++){
		cin>>time[i]>>val[i];
	}
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=M;i++){//i是物品 ,留第一行用来放0 
		for(int j=T;j>=0;j--){//倒序的 
			if(time[i]<=j)
			dp[j]=max(dp[j],dp[j-time[i]]+val[i]);
		}
	}
	cout<<dp[T];
}

一维和二维数组都有,对于为什么能用简化空间复杂度用一维数组 ,再给自己强调以下:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-time[i]]+val[i]),对于这个状态转移方程,第i次状态仅由第i-1次得到,于是数组可以重复利用,可以采用两个数组交替使用或者直接一维数组。对于循环逆向进行的通俗解释是,如果循环是正向进行的,那么dp[j-time[i]]会被 “其他状态” 覆盖,这个其他状态其实就是被多选了的状态,在完全背包中也有所体现。这里不多写了。ps:我确实不太聪明,这个逆向都理解好久。。

给我自己的提醒:dp数组要开足够大,不要忘了初始化,一般都从1开始,把0让出来,方便选第一个物品的时候编程。如果重量比较大,可以把重量和价值换一下,换成对于价值求最小重量。

例题:洛谷1049

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int dp[50][20000];
int main(){
	int V,n;
	int v[50];
	cin>>V;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>v[i];
	}
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=n;i++){
		for(int j=0;j<=V;j++){
			if(v[i]>j){//体积超过了总体积 
				dp[i][j]=dp[i-1][j]; 
			}else{
				dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+v[i]);
			} 
		}
	}
	cout<<V-dp[n][V];	
}

取消了价值设定,只留下体积,要求体积最大,把体积当价值就行了。

给我自己的提醒:很多东西都可以放全局去

例题:洛谷1064

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int dp[35000];
int zw[100],zv[100],fw[100][3],fv[100][3];
int main(){
	int n,m;
	cin>>n>>m;
	int a,b,c;
	for(int i=1;i<=m;i++){
		cin>>a>>b>>c;
		if(c==0){
			zw[i]=a;
			zv[i]=a*b; 
		}else{
			fw[c][0]++;//保存附件数量
			fw[c][fw[c][0]]=a;
			fv[c][fw[c][0]]=a*b;
		}
	}
	for(int i=1;i<=m;i++){
		for(int j=n;j>=0;j--){
			if(zw[i]<=j){
				dp[j]=max(dp[j],dp[j-zw[i]]+zv[i]);//只要主件 
				if(fw[i][1]){//有附件1的话要主件和附件1
					if(j>=zw[i]+fw[i][1])
					dp[j]=max(dp[j],dp[j-zw[i]-fw[i][1]]+zv[i]+fv[i][1]); 
				} 
				if(fw[i][2]){//有附件2的话
					if(j>=zw[i]+fw[i][2])//要主件和附件2
					dp[j]=max(dp[j],dp[j-zw[i]-fw[i][2]]+zv[i]+fv[i][2]); 
					if(j>=zw[i]+fw[i][2]+fw[i][1])//主12都要 
					dp[j]=max(dp[j],dp[j-zw[i]-fw[i][2]-fw[i][1]]+zv[i]+fv[i][2]+fv[i][1]); 
				}				
			}
		} 
	}
	cout<<dp[n];
}

是个带依赖的01背包?限制条件是有些物品不能单独放进背包,需要和绑定的主体一起放进去,主体则不受限制。这个题思维上就用一维数组方便一点了,以主体看作一个整的物品,放进去的条件多写亿点就行了

给我自己的提醒:注意小优化,比如所有的条件的体积都满足一个条件,就可以在循环中提前结束,还有有的时候可以不用特判,可以多想想方便写代码

其他:注意背包九讲原文中初始化的细节问题(满包)


完全背包问题:

递推公式:dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]),是通过变形

Max(dp[i][j-k*w[i]]+k*v[i]|0≤k)得到的,变形过程有点复杂略掉,对于正序求解的解释在01背包写过了。完全背包解除了只能选一个的限制,当然可以让dp[j-time[i]]被覆盖掉,且必须让它被覆盖掉。

例题:洛谷1616

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
ll dp[10000050];
int main(){
	int M,T;
	int time[10050],val[10050];	
	cin>>T>>M;
	for(int i=1;i<=M;i++){
		cin>>time[i]>>val[i];
	}
	memset(dp,0,sizeof(dp));
	for(int i=1;i<=M;i++){
		for(int j=time[i];j<=T;j++){ 
			dp[j]=max(dp[j],dp[j-time[i]]+val[i]);
		}
	}
	cout<<dp[T];
}

最长公共子序列问题

例题:洛谷1439

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int dp[100050],a[100050],b[100050],c[100050];
int main(){
	int n;
	cin>>n;
	for(int i=0;i<n;i++){
		cin>>a[i];
		c[a[i]]=i;
	}
	for(int i=0;i<n;i++){
		cin>>b[i];
	}
	//int dp[10];
	//int a[10]={1,4,7,2,5,9,10,3};
	memset(dp,inf,sizeof(dp));
	for(int i=0;i<n;i++){
		*lower_bound(dp,dp+n,c[b[i]])=c[b[i]];
	}
	printf("%d",lower_bound(dp,dp+n,inf)-dp);
}

代码做法并不是动态规划,这题用朴素的dp会超时,时间复杂度是n^2。只是有这样的做法。

dp小代码:

	for(int i=1;i<=n;i++)
		for(int j=1;j<n;j++)
			if(a[i]=b[j])
			dp[i][j]=dp[i-1][j-1]+1;
			else
			dp[i][j]=max(dp[i-1][j],dp[i][j-1]);

多重部分和问题:

没找到例题,转载个比较详细的多重部分和讲解


最长上升子序列问题:

同样插个链接:LIS

文章中提到的第二种方法的小代码参考:

int main(){
	int dp[10];
	int a[10]={1,4,7,2,5,9,10,3};
	memset(dp,inf,sizeof(dp));
	for(int i=0;i<8;i++){
		*lower_bound(dp,dp+8,a[i])=a[i];//直接把查找的地方替换
	}
	printf("%d",lower_bound(dp,dp+8,inf)-dp);
}

其他用法:求有序数组中k的个数:upperbound(a,a+n,k)-l_b(a,a+n,k)

例题:洛谷1439

代码之前贴过了。思想:要求的最长公共子序列,但是给出的是数字1到n的排列,如果将上面的序列重新标成有序的号,如3 2 1 4 5-> a b c d e,则下面的一一对应:1 2 3 4 5-> c b a d e这样以来,这个这个LIS不会变,在这个例子里是145(cde),而在第一个序列我们手动转化成了上升序列,则在第二个里也应该是上升序列,即LIS。对应方法也很简单,如a[]储存第一个,b[]储存第二个,c[]是b对应之后的序列,则输入的时候:c[a[i]]=i,然后c[b[i]]就是对应的序列了

给自己的提醒:*lower_bound(dp,dp+8,a[i])=a[i];直接把查找的地方替换


计数dp问题:

复习链接:计数dp

例题:洛谷1025

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const int Max=1000;
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int dp[Max][Max]={0};
int main(){
	int n,m;
	
	cin>>n>>m;
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		for(int j=0;j<=n;j++){
			if(j-i>=0){
				dp[i][j]=dp[i-1][j]+dp[i][j-i];
			}else
			dp[i][j]=dp[i-1][j];
		}
	}
	cout<<dp[m][n]-dp[m-1][n];	
}

划分数的二维做法储存的是不超过m种划分的数,要求刚好m种划分的话减去就行了,如果求总的划分还可以简化成一维数组。更简单易懂的方法:csdn,如果要求总的划分数加起来就行了


多重集组合数:

复习链接:csdn

part2:树形结构


优先队列:

使用STL就行了,的priority_queue函数创建的是一个大顶堆

可以采用priority_queue<int,vector<int>,greater<int>>创建一个小根堆

例题:poj2431

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const int Max=10005;
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
bool cmp(pair<int,int> p1,pair<int,int> p2){
	return p1.first<p2.first;
}
int main(){
	pair<int,int>stop[Max];
	//int dis[10005],val[10005];
	int N,L,P;
	priority_queue<int>q;
	scanf("%d",&N);
	for(int i=0;i<N;i++){
		scanf("%d %d",&stop[i].first,&stop[i].second);
	}
	scanf("%d %d",&L,&P);
	stop[N].first=L,stop[N].second=0;
	for(int i=0;i<N;i++){
		stop[i].first=L-stop[i].first;
		//cout<dis[i]<<' ';
	}
	N++;
	sort(stop,stop+N,cmp);
	int ans=0;int p=0;int t=P;
	for(int i=0;i<N;i++){
		int d=stop[i].first-p;
		while(t<d){//油不够 
			if(q.empty()){
				printf("-1\n");
				return 0;
			}
			t+=q.top();
			q.pop();
			ans++;
		}
		t-=d;
		p=stop[i].first;
		q.push(stop[i].second);
	}
	printf("%d\n",ans);
}

ps:把终点也当成一个加油站会好写很多

例题:poj3253

代码在不存在的训练纪录1里面,找不到了

在之前的练习中用贪心法做过,这里用小根堆小优化一下取最短和次短的模板

给自己的提醒:开longlong 记得改%lld


二叉搜索树:

优点:高效插入/删除数值,查询是否包含数值,时间复杂度为logn

树的代码实现:略。C++中提供用平衡二叉树维护的set和map。使用见代码

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const int Max=10005;
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int main(){
	set<int> s;
	s.insert(3);
	s.insert(2);
	s.insert(1);
	
	set<int>::iterator ite;
	ite=s.find(1);
	if(ite==s.end())
	printf("not found\n");
	else
	printf("found\n");
	if(s.count(3)!=0)
	printf("found\n");
	else
	printf("%not found\n");
	
	for(ite=s.begin();ite!=s.end();ite++)
	printf("%d\n",*ite);
	
	s.erase(3); 
	printf("map:\n");
	map<int,float> m;
	m.insert(make_pair(1,1.25));
	m.insert(make_pair(50,50.25));
	m[100]=100.25;
	map<int,float>::iterator it;
	it=m.find(1);
	printf("%f\n",it->second);
	it=m.find(2);
	if(it==m.end())
	printf("not found\n");
	else
	printf("%f\n",it->second);
	
	printf("%f\n",m[100]);
	m.erase(100);

	for(it=m.begin();it!=m.end();it++)
	printf("%d %f\n",it->first,it->second);
}

*二叉树的退化:当按照1,2,3,4,5……插入节点时,二叉树会退化成链表,时间复杂度化为on,而平衡二叉树就避免了这样的问题,所以还是用stl库里的函数方便


并查集:

优点:查询a,b是否属于同一组,合并a,b所在组

代码实现细节:

*初始化:n个元素代表n个节点,没有边

*合并:将一个组的向另一个组的连边,两棵树就变成一棵树了。为了防止像二叉树一样的退化现象发生,可以做以下处理:对于每棵树,纪录高度,合并时如果高度不同,则小高度向大高度连边

*查询:查询根是否是同一个即可

*路径压缩:对于每个节点,如果操作中走到了一次根节点,则直接把它连到根

为了简单,高度变化了也不管纪录的树高

以上,平均复杂度为O(α(n)),是什么阿克曼函数的反函数,反正比logn快就是了。

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const int Max=1000;
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);
int par[Max];
int ranK[Max];
void init(int n){
	for(int i=0;i<n;i++){
		par[i]=i;
		ranK[i]=0;
	}
}
int find(int x){
	if(par[x]==x)
	return x;
	else
	return par[x]=find(par[x]);//已经进行了压缩 
}
void unite(int x,int y){
	x=find(x);
	y=find(y);
	if(x==y)
	return ;
	if(rank[x]<rank[y]){
		par[x]=y;
	}else{
		par[y]=x;
		if(rank[x]==rank[y])
		rank[x]++;	
	}
}
bool same(int x,int y){
	return find(x)==find(y);
}
int main(){

}

例题:poj1182

题目大意:动物都属于ABC三种,存在A吃B,B吃C,C吃A的关系。给出两种信息:x吃y/x,y是同类,问有多少信息和之前矛盾(不矛盾就当正确的)

思路:在并查集的基础上添加了一层关系,有两种办法解题,第一种是把储存几并查集的数组变成结构体来储存更多信息,然后在合并,路径压缩的时候变根的同时变关系,思路比较复杂。第二种是我用(学)的方式,很巧妙,并不是让一个节点储存更多信息,而是为每个动物多准备了两个节点,共三个,分别对应了动物的关系(ABC)。对于同类关系,先检查是否存在吃的关系(检查x-a和y-b或y-c是否在同一组里面),然后合并xa,ya和xb,yb和xc,yc,对于吃的关系,先检查是否存在同类/被吃的关系,然后何必跟xa,yb和xb,yc和xc,ya关系。至于检查的部分为什么可以只检查xa,yb不用检查xb,yc(以第一个而言)是因为合并的时候abc都进行了合并,就可以只检查其中一组了。

注意事项:这题比较巧,以后遇到这种如果不能巧妙的设出并查集元素还是老实用结构体推关系把,虽然比较难受


part3:图论基础

有向无圈图叫DAG,对于每个顶点标号,如果存在vi到vj的边时就有i<j成立,这样的编号方式叫做拓扑序。如果把顶点按拓扑序排列,那么所有的边都是从左往右指的,这样有些问题可以用dp求解。求解拓扑序的算法叫拓扑排序

拓扑排序例题:zcmu2153

#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<vector>
#include<queue>
#include<map>
#include<set>
#include<stack>
#include<iomanip>
using namespace std;
typedef long long ll;
const long long ll_inf=0x3f3f3f3f3f3f3f3f;
const int inf=0x3f3f3f3f;
const double pi=acos(-1.0);
const int Mod=1e9+7;
const int Max=1000;
//cout<<setw(8)<<fixed<<setprecision(3);
//ios::sync_with_stdio(false);

int main(){
	int in[50]={0};
	priority_queue<int,vector<int>,greater<int> >q;
	vector<int>edge[50];
	char s[5];
	set<int>k;
	while(scanf("%s",s)!=EOF){
		k.insert(s[0]-'A');
		k.insert(s[2]-'A');
		if(s[1]=='>'){
			in[s[2]-'A']++;
			edge[s[0]-'A'].push_back(s[2]-'A');
		}else{
			in[s[0]-'A']++;
			edge[s[2]-'A'].push_back(s[0]-'A');	
		}
	}
	for(int i=0;i<=30;i++)//n:节点的总数
	if(in[i]==0&&k.count(i)) q.push(i);//将入度为0的点入队列
	vector<int>ans;
	while(!q.empty()){
		int p=q.top();
		q.pop();//选一个入度为0的点,出队列
		ans.push_back(p);	
		for(int i=0;i<edge[p].size();i++){
			int y=edge[p][i];
			in[y]--;
			if(in[y]==0)
			q.push(y);
			//cout<<y<<'?';
		}	
	}	
	if(ans.size()==k.size()){
 		for(int i=0;i<ans.size();i++)
            printf( "%c",ans[i]+'A');
       	printf("\n");		
	}
	else printf("No Answer!\n");
}

大意:给出多组人名的身高比(A>B),求字典序最小的最终排序。

思路:拓扑排序的简单应用,不过最后要求字典序最小的答案,把队列改成有限队列即可。

给自己的提醒:给的是人名可以使用set统计全部的数量,就是一个灵活运用的问题

图论无感,先跳过把,写下周的了

为什么还在学这些简单内容呢?当然是因为之前都学的不扎实全忘了。。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值