数据结构荣誉课-第七次实验-解题报告

一、序列调度

题目

有一个N个数的序列A:1,2,……,N。有一个后进先出容器D,容器的容量为C。如果给出一个由1到N组成的序列,那么可否由A使用容器D的插入和删除操作得到。

输入格式:

第1行,2个整数T和C,空格分隔,分别表示询问的组数和容器的容量,1≤T≤10,1≤C≤N。

第2到T+1行,每行的第1个整数N,表示序列的元素数,1≤N≤10000。接下来N个整数,表示询问的序列。

输出格式:

T行。若第i组的序列能得到,第i行输出Yes;否则,第i行输出No,1≤i≤T。

输入样例:

  • 2 2
  • 5 1 2 5 4 3
  • 4 1 3 2 4

输出样例:

  • No
  • Yes

思路

根据题目“后进先出”的叙述,可知本题中的容器D为栈,题目所问的是在容器容量有限制的情况下,能否对顺序输入的1,2,…,n序列,通过合理的栈操作得到所要求的输出序列。下面以样例中的第一个为例作为演示:(所要得到的序列为1 2 5 4 3)

  • 开始时,1入栈,接着检测栈顶元素是否和当前所需序列的第一个相等,发现相等(1 2 5 4 3),于是1弹栈。此时栈内();
  • 接着,2入栈,检测(2 5 4 3),相等,2弹栈,此时栈内();
  • 接着,3入栈,检测(2 5 4 3),不相等,不做任何操作,此时栈内(3);
  • 接着,4入栈,检测(2 5 4 3),不相等,不做任何操作,此时栈内(4,3);
  • 接着,5入栈,检测(2 5 4 3),不相等,不做任何操作,此时栈内(5,4,3),发现超过了规定的容量,输出"No"。

参考代码

#include "iostream"
using namespace std;

int T;//询问次数
int C;//容量

int a[10001];//模拟的栈
int b[10001];//存储目标序列

int main()
{
	cin>>T>>C;

	for(int i=0;i<T;i++)
	{
		int n;
		cin>>n;

		for(int i=0;i<n;i++)//读入检测序列并初始化模拟栈
		{
			cin>>b[i];
			a[i]=0;
		}

		int sum=0;//已经得到的数字的个数,也是当前需要检测的检测序列下标,以下成为检测指针
		int num=1;//将要入栈的数字
		int index=1;//当前将要入栈元素的下标,也是当前栈顶元素的下标+1,也是当前栈中元素的个数+1
		while(1)
		{
			while(sum!=n&&a[index-1]==b[sum])//如果当前栈顶元素等于检测指针所指的元素,不断弹栈
			{
				index--;//弹栈
				sum++;//检测指针+1
			}
			if(sum==n)//所需的序列已经得到
			{
				cout<<"Yes"<<endl;
				break;
			}
			if(index>C)//模拟栈已满
			{
				cout<<"No"<<endl;
				break;
			}
			else//入栈
				a[index++]=num++;
		}
	}
}

二、最大最小差

题目

对n 个正整数,进行如下操作:每一次删去其中两个数 a 和 b,然后加入一个新数:a*b+1,如此下去直到 只剩下一个数。所有按这种操作方式最后得到的数中,最大的为max,最小的为min,计算max-min。

输入格式:

第1行:n,数列元素的个数,1<=n<=16。

第2行:n 个用空格隔开的数x,x<=10。

输出格式:

1行,所求max-min。

输入样例:

  • 3
  • 2 4 3

输出样例:

  • 2

思路

本题中的整数全部是正数,因此得到的最大值一定是当前序列中两个较大的值相乘+1得到的;相应的最小值也一定是当前序列中两个较小的值相乘+1得到的。于是我们只需要两个优先队列,一个最大优先,一个最小优先。每次取两个队首元素相乘+1并再次入队,直至队中元素数为1。

参考代码

#include "iostream"
#include "algorithm"
#include "vector"
#include "queue"
using namespace std;

priority_queue<int,vector<int>,less<int> > q1;//最小优先队列
priority_queue<int,vector<int>,greater<int> > q2;//最大优先队列

int N;

int main()
{
	cin>>N;

	int tmp;
	for(int i=0;i<N;i++)
	{
		cin>>tmp;
		q1.push(tmp);
		q2.push(tmp);
	}

	int a,b;
	for(int i=0;i<N-1;i++)//进行N-1次
	{
		a=q1.top();//取两个最小
		q1.pop();
		b=q1.top();
		q1.pop();
		q1.push(a*b+1);//新数据入队

		a=q2.top();//取两个最大
		q2.pop();
		b=q2.top();
		q2.pop();
		q2.push(a*b+1);//新数据入队
	}

	int Max=q2.top();//队首即最大值
	int Min=q1.top();//队首即最小值

	cout<<Max-Min<<endl;

}

三、二叉树最短路径长度

题目

给定一棵二叉树T,每个结点赋一个权值。计算从根结点到所有结点的最短路径长度。路径长度定义为:路径上的每个顶点的权值和。

输入格式:

第1行,1个整数n,表示二叉树T的结点数,结点编号1…n,1≤n≤20000。

第2行,n个整数,空格分隔,表示T的先根序列,序列中结点用编号表示。

第3行,n个整数,空格分隔,表示T的中根序列,序列中结点用编号表示。

第4行,n个整数Wi,空格分隔,表示T中结点的权值,-10000≤Wi≤10000,1≤i≤n。

输出格式:

1行,n个整数,表示根结点到其它所有结点的最短路径长度。

输入样例:

  • 4
  • 1 2 4 3
  • 4 2 1 3
  • 1 -1 2 3

输出样例:

  • 1 0 3 3

思路

  1. [造树]
  • 本题需要根据先根序列和中根序列构造二叉树。

  • 我们先思考一下,只根据先根序列为什么不能构造唯一一颗二叉树?因为我们无法确定当前结点A后面的一个结点B到底是不是它的儿子,如果是,是左儿子还是右儿子。(当前结点可能是叶节点,下一个结点为它的右兄弟或者父亲的右兄弟;当前结点可能没有左儿子,但是有右儿子,于是下一节点便是它的右儿子)

  • 接着我们再思考,有了中根序列,为什么就能解决上述问题了呢?因为中根序列中,我们可以从根节点A开始给序列分块,根节点A左边的是它的左子树中根序列,根节点A右边的是他的右子树中根序列。于是我们再分别检测A的左右子树中根序列,便可以确定B到底是不是它的儿子,是什么儿子(如果左右子树序列中都未找到B,那么B就不是A的儿子;如果左子树序列中找到了B,那么B是A的左儿子;如果左子树序列中找到了B,那么B是A的右儿子)。接着再对左右子树序列进行类似的操作,就可以确定所有结点之间的父子关系了。

  • 于是我们就可以总结出根据先根序列和中根序列构造二叉树的方法了:假设当前要构造的结点为A,检测的区间为[a,b]。首先再[a,b]中寻找A,如果没找到,说明A不是上一个结点(初始时这个结点便是一个假想的哨兵结点)这个方向(左或右)的儿子,同时说明上一个节点这个方向的儿子为空,赋值为NULL;如果在c位置找到了,说明A是上一个结点这个方向的儿子,创建新结点,并递归构造A的左右子树(左子树的检测区间为[a,c-1],右子树的检测区间为[c+1,b])。

  • 造树的参考代码如下:

    const int MaxN=20001;
    
    int Pre[MaxN];//先根序列
    int In[MaxN];//中根序列
    
    struct TreeNode//树结点
    {
    	int data;
    	TreeNode *Left;
    	TreeNode *Right;
    };
    TreeNode T[MaxN];
    
    void BuildTree(TreeNode *&t,int PreIndex,int Size,int InBegin,int InEnd)
    {//当前要构造结点的指针,当前要构造的先根序列的下标,先(中)根序列的长度,检测中根序列的左区间端点,检测中根序列的右区间端点
    	int InMid=-1;//定位当前结点的值在中根序列区间的位置,如果找完后的值仍为-1,则说明没找到
    
    	static int index=1;//用来指示当前先根序列已经构造了多少
    	
    	if(index>Size)//此时说明整个先根序列已经构造完毕
    	{
    		t=NULL;
    		return;
    	}
    
    	for(int i=InBegin;i<=InEnd;i++)//在中根序列的区间内找当前要构造的值
    		if(Pre[index]==In[i])
    		{
    			InMid=i;
    			break;
    		}
    
    	if(InMid==-1)//如果没找到,赋值为空
    	{
    		t=NULL;
    		return;
    	}
    	else
    	{
    		t=T+index;//构造新结点
    		t->data=Pre[index];
    		
    		index++;//计数+1
    		
    		//递归构造左右子树
    		BuildTree(t->Left,index,Size,InBegin,InMid-1);
    		BuildTree(t->Right,index,Size,InMid+1,InEnd);
    	}
    }
    
  1. [求最短路径长度]
  • 由于二叉树是一个有向无回路的图,因此根节点到某点的路径唯一,求最短 路径长度也就是从根走到这的长度,这里选择了DFS遍历(其实就是先根遍历)。

  • 求路径长度的参考代码如下:

    int Cost[MaxN];//每个结点的权值
    int Ans[MaxN];//储存答案的数组
    
    void DFS(TreeNode *t,int sum)
    {
    	if(!t)
    		return;
    	else
    	{
    		Ans[t->data]=sum+Cost[t->data];
    		sum=Ans[t->data];
    		DFS(t->Left,sum);
    		DFS(t->Right,sum);
    	}
    }
    

参考代码

#include "iostream"
using namespace std;

const int MaxN=20001;
int Pre[MaxN];
int In[MaxN];
struct TreeNode
{
	int data;
	TreeNode *Left;
	TreeNode *Right;
};
TreeNode T[MaxN];
int Cost[MaxN];
int Ans[MaxN];

void BuildTree(TreeNode *&t,int PreIndex,int Size,int InBegin,int InEnd);//这里从略

void DFS(TreeNode *t,int sum);//这里从略

int N;
TreeNode *t;

int main()
{
	cin>>N;

	for(int i=1;i<=N;i++)
		cin>>Pre[i];
	for(int i=1;i<=N;i++)
		cin>>In[i];
	for(int i=1;i<=N;i++)
		cin>>Cost[i];

	BuildTree(t,1,N,1,N);

	DFS(t,0);

	for(int i=1;i<N;i++)
		cout<<Ans[i]<<" ";
	cout<<Ans[N]<<endl;
}

四、方案计数

题目

组装一个产品需要 n 个零件。生产每个零件都需花费一定的时间。零件的生产可以并行进行。有些零件的生产有先后关系,只有一个零件的之前的所有零件都生产完毕,才能开始生产这个零件。如何合理安排工序,才能在最少的时间内完成所有零件的生产。在保证最少时间情况下,关键方案有多少种,关键方案是指从生产开始时间到结束时间的一个零件生产序列,序列中相邻两个零件的关系属于事先给出的零件间先后关系的集合,序列中的每一个零件的生产都不能延期。

输入格式:

第1行,2个整数n和m,用空格分隔,分别表示零件数和关系数,零件编号1…n,1≤n≤10000, 0≤m≤100000 。

第2行,n个整数Ti,用空格分隔,表示零件i的生产时间,1≤i≤n,1≤Ti≤100 。

第3到m+2行,每行两个整数i和j,用空格分隔,表示零件i要在零件j之前生产。
输出格式:

第1行,1个整数,完成生产的最少时间。

第2行,1个整数,关键方案数,最多100位。

如果生产不能完成,只输出1行,包含1个整数0。

输入样例:

  • 4 4
  • 1 2 2 1
  • 1 2
  • 1 3
  • 2 4
  • 3 4

输出样例:

  • 4
  • 2

思路

  1. 本题题意很好理解,就是求关键路径上各点的权值和并输出关键路径的条数。

  2. 题目中给的是每个点的权重,因此我们需要把点权转化为边权。这里用到的方法是引入一个虚源点和一个虚汇点,向虚源点和所有入度为零的点之间加一条边,向虚汇点和所有出度为零的点之间加一条边,接着将每个点的权值向后推到边上,其中虚源点的权重设为0。例如本题样例中转化后的AOE网为:

    0
    1
    1
    2
    2
    1
    0_虚源
    1
    2
    3
    4
    5_虚汇
  3. 对上面的AOE网求关键路径直接看代码就可以了。

  4. 接着要求关键路径的条数,这里用的方法是用DFS遍历,遍历的条件是该节点的ve=vl,当遍历到虚汇点时,Ans计数+1。

  5. 为了防止某些结点被多次访问,我们需要对上述DFS进行一些优化。当我们访问完某一个结点后,对该节点置标记,该标记指示该结点后面关键路径的条数(例如本例中1后面的关键路径有两条,因此当DFS(1)结束后,结点1会被置标记2),当我们再次访问该结点时,只需直接加上该节点的标记,而不需要继续遍历该结点及其后续结点。这里的做法是,在DFS(x)的开始,记录Ans的值,当DFS(x)结束后,Ans值会改变,其差值即为该点后面关键路径的条数
    []:根据上述置标记的方法,虚汇点的标记为0,这表明虚汇点会被视为未被访问的结点,因此实际代码中置标记都多置了1位(例如本例中1实际会被置标记3)。

  6. 上述DFS的参考代码如下;

    int Ans;//储存答案
    int HasDefine[MaxN];//标记
    
    void DFS(int x)
    {
    	if(x==N+1)//如果遍历到虚汇点,Ans+1
    	{
    		Ans++;
    		return;
    	}
    
    	int ans=Ans;//记录当前的Ans
    
    	for(vector<pair<int,int> >::iterator it=v[x].begin();it!=v[x].end();it++)//遍历其所有邻边
    	{
    		if(HasDefine[it->first])//如果已经求出其后面的关键路径数
    		{
    			Ans+=(HasDefine[it->first]-1);//直接加上即可,注意-1
    			continue;
    		}
    		if(ve[it->first]==vl[it->first])//只有ve=vl才是关键路径上的点,才要遍历它的临边
    			DFS(it->first);
    	}
    
    	HasDefine[x]=Ans-ans+1;//置标记
    }
    
  7. 另外本题提示最终的关键路径的条数不多于100位,实际要求我们使用高精度进行计算,于是上面代码中的Ans,ans和HasDefine[]都需要声明称MyInt(自定义高精度数)。

  8. MyInt类的参考代码如下(写的比较墨迹):

    namespace Int
    {
    	class MyInt
    	{
    	public:
    		vector<int> v;//储存数组,一位一存
    		int Flag;//标志该数字是否是0
    
    		MyInt(int=0);//构造函数
    		MyInt operator=(MyInt a);//重载赋值运算符
    		MyInt operator+(MyInt a);//重载+运算符
    		MyInt &operator+=(MyInt a);//重载+=运算符
    		MyInt operator-(MyInt a);//重载-运算符,该运算仅针对本题(被减数一定大于减数)
    		friend ostream &operator<<(ostream &cout,MyInt &a);//重载左移运算符
    	};
    
    	ostream &operator<<(ostream &cout,MyInt &a)
    	{
    		for(int i=0;i<=a.v.size()-1;i++)
    			cout<<a.v[i];
    
    		return cout;
    	}
    	
    	MyInt::MyInt(int num)
    	{
    		int tmp=num;
    
    		if(tmp==0)//默认赋值为0
    		{
    			Flag=0;
    			v.push_back(0);
    		}
    		else
    		{
    			Flag=1;
    			while(tmp)//一位一位存,存完之后是倒序
    			{
    				v.push_back(tmp%10);
    				tmp/=10;
    			}
    
    			reverse(v.begin(),v.end());//反转
    		}
    
    	}
    	
    	MyInt MyInt::operator=(MyInt a)
    	{
    		this->v.assign(a.v.begin(),a.v.end());
    		this->Flag=a.Flag;
    
    		return *this;
    	}
    	
    	MyInt MyInt::operator+(MyInt a)
    	{
    		MyInt m;//相加的结果
    		m.v.clear();
    		m.Flag=a.Flag;
    
    		int i=this->v.size()-1;
    		int j=a.v.size()-1;
    
    		int flag=0;//标记有没有进位
    
    		while(i>=0&&j>=0)
    		{
    			int tmp=this->v[i]+a.v[j];//当前位之和
    			if(flag==1)//如果之前有进位,当前位再+1
    			{
    				flag=0;
    				tmp+=1;
    			}
    			if(tmp>=10)//如果当前位>=0,置进位标记,当前位-10
    			{
    				flag=1;
    				tmp-=10;
    			}
    			m.v.push_back(tmp);
    			i--;
    			j--;
    		}
    		//将剩余的数字存入结果
    		while(i>=0)
    		{
    			int tmp=this->v[i];
    			if(flag==1)
    			{
    				flag=0;
    				tmp+=1;
    			}
    			if(tmp>=10)
    			{
    				flag=1;
    				tmp-=10;
    			}
    			m.v.push_back(tmp);
    			i--;
    		}
    		while(j>=0)
    		{
    			int tmp=a.v[j];
    			if(flag==1)
    				flag=0,	tmp+=1;
    
    			if(tmp>=10)
    				flag=1,tmp-=10;
    
    			m.v.push_back(tmp);
    
    			j--;
    		}
    
    		if(flag)
    		{
    			m.v.push_back(1);
    			flag=0;
    		}
    		reverse(m.v.begin(),m.v.end());
    		return m;
    	}
    	MyInt &MyInt::operator+=(MyInt a)
    	{
    		*this=*this+a;
    		return *this;
    	}
    	MyInt MyInt::operator-(MyInt a)//和加法思路一摸一样,被迫又写了一遍
    	{
    		MyInt m;
    		m.v.clear();
    		m.Flag=a.Flag;
    
    		int i=this->v.size()-1;
    		int j=a.v.size()-1;
    
    		int flag=0;
    
    		while(i>=0&&j>=0)
    		{
    			int tmp=this->v[i]-a.v[j];
    			if(flag==1)
    			{
    				flag=0;
    				tmp-=1;
    			}
    			if(tmp<0)
    			{
    				flag=1;
    				tmp+=10;
    			}
    			m.v.push_back(tmp);
    			i--;j--;
    		}
    
    		while(i>=0)
    		{
    			int tmp=this->v[i];
    			if(flag==1)
    			{
    				flag=0;
    				tmp-=1;
    			}
    			if(tmp<0)
    			{
    				flag=1;
    				tmp+=10;
    			}
    			m.v.push_back(tmp);
    			i--;
    		}
    		//注:这里没有考虑while(j>=0)的情况,因为这个类没有存负数的功能,但本题中被减数一定大于减数,此处无影响
    		
    		int f=m.v.size()-1;
    		while(!m.v[f])//把末尾的0删除(防止最后出现00012这样的数字)
    		{
    			m.v.pop_back();
    			f--;
    		}
    
    		reverse(m.v.begin(),m.v.end());
    
    		return m;
    	}
    
    }
    
    

参考代码

#include "iostream"
#include "vector"
#include "algorithm"
#include "stack"
using namespace std;
namespace Int{//...这里从略}
using namespace Int;

const int MaxN=1e4+4;

int N;
int M;
vector<pair<int,int> > v[MaxN];//顶点数组,其中pair(邻点坐标,路径长度)
int Order[MaxN];//拓扑序列
int Count[MaxN];//存入度

void TopoOrder()//拓扑排序
{
	stack<int> s;
	s.push(0);//虚源点入栈

	for(int i=0,k=0;i<=N+1;i++)
	{
		if(s.empty())//此时说明出现回路,无法满足要求
		{
			cout<<0<<endl;
			exit(0);
		}

		int x=s.top();
		s.pop();
		Order[k++]=x;//存入拓扑序

		for(vector<pair<int,int> >::iterator it=v[x].begin();it!=v[x].end();it++)//处理所有邻点
		{
			Count[it->first]--;
			if(Count[it->first]==0)
				s.push(it->first);
		}

	}
}

int ve[MaxN];//最早开始时间
int vl[MaxN];//最晚开始时间
void CriticalPath()//求关键路径
{
	TopoOrder();

	for(int i=0;i<=N;i++)//正拓扑序计算最早发生时间
		for(vector<pair<int,int> >::iterator it=v[Order[i]].begin();it!=v[Order[i]].end();it++)
			if(ve[it->first]<ve[Order[i]]+it->second)
				ve[it->first]=ve[Order[i]]+it->second;

	for(int i=0;i<=N+1;i++)//将所有最晚发生时间赋值为最大的最早发生时间,即虚汇点的最早发生时间
		vl[i]=ve[N+1];

	for(int i=N;i>=0;i--)//逆拓扑序计算最晚发生时间
		for(vector<pair<int,int> >::iterator it=v[Order[i]].begin();it!=v[Order[i]].end();it++)
			if(vl[Order[i]]>vl[it->first]-it->second)
				vl[Order[i]]=vl[it->first]-it->second;


}

MyInt Ans;
MyInt HasDefine[MaxN];
void DFS(int x)
{
	if(x==N+1)
	{
		Ans+=1;
		return;
	}

	MyInt ans=Ans;

	for(vector<pair<int,int> >::iterator it=v[x].begin();it!=v[x].end();it++)
	{
		if(HasDefine[it->first].Flag)
		{
			Ans+=(HasDefine[it->first]-1);
			continue;
		}
		if(ve[it->first]==vl[it->first])
			DFS(it->first);
	}

	HasDefine[x]=Ans-ans+1;

}

int Cost[MaxN];//每个点的权值
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin>>N>>M;

	int cost;
	for(int i=1;i<=N;i++)
		cin>>Cost[i];

	int index_u,index_v;
	for(int i=1;i<=M;i++)
	{
		cin>>index_u>>index_v;
		v[index_u].push_back(make_pair(index_v,Cost[index_u]));
		Count[index_v]++;//入度+1
	}

	for(int i=1;i<=N;i++)//将所有入度为0的点和虚源点之间加一条边
		if(Count[i]==0)
		{
			v[0].push_back(make_pair(i,0));
			Count[i]++;
		}


	for(int i=1;i<=N;i++)//将所有出度为0的点和虚汇点之间加一条边
		if(!v[i].size())
		{
			v[i].push_back(make_pair(N+1,Cost[i]));
			Count[N+1]++;
		}

	CriticalPath();
	
	DFS(0);

	cout<<vl[N+1]<<endl;
	cout<<Ans<<endl;
}
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值