数据结构之算法入门经典

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhangquan2015/article/details/82915889

输入

  1. 输入完毕先按enter,再按ctrl+z,最后按enter,即可结束输入。
int x;
while(scanf("%d",&x)==1)
{
	//程序
}
  1. 使用文件输入:
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);

预编译

#define LOCAL
int main()
{
#ifdef LOCAL
	freopen("input.txt","r",stdin);
	freopen("output.txt","w",stdout);
#endif
	//程序
}

类型推导

vector<int> vec;
vector<int>::iterator cit = vec.begin();

在C++11中可以写成:

vector<int> vec;
auto cit=vec.begin();

容器的for循环遍历

for(const auto&p: vec)
	cout<<p<<endl;

初始化容器

C++98中需要一个个push_back,C++11中初始化容器:

vector<string> vec{1,2,3};
map<string,string> dict{{"ABC","123"},{"BCD","234"}};

哈希容器

当不需要元素排序时,可以尽量使用容器来获得更好的查找性能:

#include<unordered_map>
unordered_map<string,int> um{
	{"A",1972},{"B",1976},
	{"C",1967},{"D",1968}
};
um["E"]=1983;//添加
for(auto x:um)//迭代输出
	cout<<x.first<<x.second;

指针参数

正确:

#include<stdio.h>
void swap(int* a,int* b)
{
	int t=*a;*a=*b;*b=t;
}
int main()
{
	int a=3,b=4;
	swap(&a,&b);
	return 0;
}

数组参数

错误:sizeof(a)无法得到数组大小,传入的是地址

int sum(int a[])
{
	int ans=0;
	for(int i=0;i<sizeof(a);i++)
		ans+=a[i];
	return ans;
}

正确:

int sum(int* a,int n)
{
	int ans=0;
	for(int i=0;i<n;i++)
		ans+=a[i];
	return ans;
}
sum(a+1,3);//a[1]+a[2]+a[3]

改进:

int sum(int *begin,int* end)
{
	int n=end-begin;
	int ans=0;
	for(int i=0;i<n;i++)\
		ans+=begin[i];
	return ans;
}
sum(a+1,a+3);//a[1]+a[2]+a[3]

数组和字符串

  1. memcpy(b,a,sizeof(int)*k);//从a复制k个元素到b
  2. memset(a,0,sizeof(a));//把数组a清零

STL框架

const声明

使用const声明的数组长度,代替define。

行输入

虽然string和sstream都很方便,但string很慢,sstream更慢,谨慎使用。

#include<iostream>
#include<string>
#include<sstream>
using namespace std;
int main()
{
	string line;
	while(getline(cin,line)){
		int sum=0,x;
		stringstream ss(line);
		while(ss>>x)
			sum+=x;
		cout<<sum<<"\n";
	}
	return 0;
}

结构体

struct Point{
	int x,y;
	Point(int x=0,int y=0):x(x),y(y){}//构造函数
	//Point(int x=0,int y=0){this->x=x;this->y=y;}
}
Point operator +(const Point& A,const Point& B){
	return Point(A.x+B.x,A.y+B.y);
}

模板

int sum(int* begin,int* end){}只能求int的和,不能求double的和。

template<typename T>
T sum(T* begin,T* end){
	T* p=begin;
	T ans=0;
	for(T* p=begin;p!=end;p++)
		ans=ans+*p;
	return ans;
}

暴力求解

消防车问题:输入一个n(n<=20)个结点的无向图以及某个结点k,按照字典序从小到大顺序输出从结点1到结点k的所有路径,要求结点不能重复经过。

  1. 使用并查集判断1和k是否连通
  2. DFS搜索所1->k所有路径
  3. 对所经过的结点进行判重
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<set>
#include<vector>
#include<sstream>
//循环宏定义
#define _for(i,a,b) for(int i=(a);i<(b);++i)

using namespace std;
const int MAXN = 20 + 4;
int N, pa[MAXN];
set<int> G[MAXN];

int find_pa(int x) {
	return pa[x] == x ? x : (pa[x] = find_pa(pa[x]));
}
ostream& operator<<(ostream& os, const vector<int>& s) {
	bool first = true;
	for (const auto x : s) {
		if (first)
			first = false;
		else
			os << ' ';
		os << x;
	}
	return os;
}
//搜索src->dest所有路径
void dfs(int src, int dest, vector<int>& path, vector<string>&paths) {
	path.push_back(src);
	if (src == dest) {
		//搜到目标
		stringstream os;
			os << path;
		paths.push_back(os.str());
	}
	else
	{
		for (auto v : G[src]) {
			if (find(path.begin(), path.end(), v) != path.end())
				continue;//走出的路径上已经有v存在
			dfs(v, dest, path, paths);
		}
	}
	path.pop_back();
}
int main() {
	for (int kase = 1, from, to; scanf("%d", &N) == 1; kase++) {
		_for(i, 0, MAXN) G[i].clear(), pa[i] = i;
		while (true)
		{
			scanf("%d%d", &from, &to);
			if(from==0||to==0)
				break;
			G[from].insert(to), G[to].insert(from);
			int pf = find_pa(from), pt = find_pa(to);
			if (pf != pt)
				pa[pt] = pf;
		}
		vector<string> paths;
		vector<int> path;
		if (find_pa(1) == find_pa(N))
			dfs(1, N, path, paths);
		printf("CASE %d:\n", kase);
		for (auto& p : paths)
			puts(p.c_str());
		printf("There are %lu routes from the firestation to streercornet %d.\n", paths.size(), N);
	}
	system("pause");
	return 0;
}

背包问题(贪心法)

最优装载问题

给出n个物体,第i个物体重量为wi,选择尽量多的物体,使得总重量不超过C。

由于只关心物体的数量,所以装重的没有轻的划算,只需把所有物体的重量按照从小到大的顺序排序,依次选择每个物体,直到装不下为止。

部分背包问题

有n个物体,第i个物体的重量为wi,价值为vi,在总重量不超过C的情况下让总价值尽量高。每个物体都可以只取走一部分,价值和重量按比例计算。

一种直观的贪心策略是:优先拿“价值除以重量的值最大的”,直到重量和正好为C。

乘船问题

有n个人,第i个人重量为wi。每艘船的最大载重量均为C,且最多只能乘两个人。用最少的船装载所有人。

如果每个人都无法和他一起坐船,则他一个人一个船。否则,选择能和他一起坐船的选最重的人。

动态规划

  1. 状态和状态转移方程
  2. 最优子结构和重叠子问题
  3. 递推法和记忆化搜索
  4. DAG上的动态规划
  5. 两种状态定义方法和刷表法

动态规划的核心是:状态状态转移方程

路径规划问题

在这里插入图片描述

从第一行的数开始,每次可以往下或右下走一格,直到走到最下行,把沿途经过的数全加起来,如何走才能使得和尽量大?

如果用回溯法(剪枝),求出所有可能的路线,从中选出最优路线,此方法效率太低。

可以设值为 a ( i , j ) a(i,j) ,解为 d ( i , j ) d(i,j) d ( i + 1 , j ) d(i+1,j) 为左下最优解, d ( i + 1 , j + 1 ) d(i+1,j+1) 为右下最优解,则最优解为:
d ( i , j ) = a ( i , j ) + m a x [ d ( i + 1 , j ) , d ( i + 1 , j + 1 ) ] d(i,j)=a(i,j)+max[d(i+1,j),d(i+1,j+1)]
即寻求最优子结构,构建状态方程

记忆化搜索与递归

有了状态方程之后,可以采用如下方法计算。

  1. 递归计算,有重复计算,效率低,如图a[3,2]分支被计算两次,存在重叠子问题
int solve(int i,int j){
	return a[i][j] + (i == n ? 0 : max(solve(i+1,j),solve(i+1,j+1)));
}

在这里插入图片描述

  1. 递推计算,注意边界处理
int i,j;
for(j=1;j<=n;j++)
	d[n][j]=a[n][j];
for(i=n-1;i>=1;i--)
	for(j=1;j<=i;j++)
		d[i][j] = a[i][j] + max(d[i+1][j],d[i+1][j+1]);
  1. 记忆化搜索,用memset(d,-1,sizeof(d))把d全部初始化为-1,然后编写递归函数:
int solve(int i,int j){
	if(d[i][j] >= 0)
		return d[i][j];
	return d[i][j]=a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1)));
}

程序依然是递归,但d初始化为-1,计算过后为非负,所以可以通过判断d非负否避免不必要的重复计算。

有向无环图(DAG)动态规划

有向无环图的动态规划是学习动态规划的基础,很多问题都可以转化为DAG上的最长路、最短路或路径计数问题。

嵌套矩形问题

有n个矩形,每个矩形都可以用两个整数a、b进行描述,表示它的长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中,当且仅当a<c,b<d,或者b<c,a<d。例如,(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)内。你的任务是选出尽量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形内。如果有多解,应使序号尽量小。

可以用来建模,X可以嵌套在Y里,就从X到Y连接一条有向边。这个有向图是无环的,因为一个矩形无法直接或间接地嵌套在自己内部。无初始状态,求DAG上的最长路径

  1. 由于没有规定起点,设从i出发,第一步只能走相邻点 d ( i ) = m a x [ d ( j ) + 1 ( i , j ) E ] d(i)=max[d(j)+1|(i,j)\in E] ,记忆化搜索:
int dp(int i)
{
	int& ans=d[i];//声明一个对d[i]的引用ans
	if(ans>0)
		return ans;
	ans=1;
	for(int j=1;j<=n;j++)
		if(G[i][j])
			ans=max(ans,dp(j)+1);
	return ans;
}
  1. 选择最小的序号: d ( i ) = d ( j ) + 1 d(i)=d(j)+1 ,选择最小的j:
void print_ans(int i)
{
	printf("%d ",i);
	for(int j=1;j<=n;j++)
		if(G[i][j] && d[i]==d[j]+1)
			print_ans(j);
			break;
}

硬币问题

有n种硬币,面值分别为V1.V2,…,Vn,每种都有无限多。给定非负整数S,可以选择多少个硬币,使得面值恰好为S?输出硬币数目的最大值和最小值。1<=n<=100,0<=S<=10000,1<=Vi<=S。初始状态为S,目标状态为0。

  1. d(i)的确切含义为从结点i出发到结点0的最长路径长度。
int dp(int S){
	int& ans=d[S];
	if(ans != -1)//-1表示没有算过
		return ans;
	ans=-(1<<30);//-2^30表示无法到达
	for(int i=1;i<=n;i++)
		if(S>+V[i])
			ans=max(ans,dp(S-V[i])+1);
	return ans;
}

路径的长度可能为0,所以用-1表示“没有访问过”,初始化使用memset(d,-1,sizeof(d)),所以把if(ans>0)改成了if(ans!=-1),-2^30表示无法到达

  1. 要求最小、最大值,记忆化搜索就必须写两个。在这种情况下,用递推更加方便:
minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++){
	minv[i]=INF;
	maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
	for(int j=1;j<=n;j++){
		minv[i]=min(minv[i],minv[i-V[j]]+1);
		maxv[i]=max(maxv[i],maxv[i-V[j]]+1);
	}
printf("%d %d\n",minv[S],maxv[S]);
  1. 输出字典序的最小方案,调用print_ans(min,S),最大为print_ans(max,S)
void print_ans(int* d,int S){
	for(int i=1;i<=n;i++)
		if(S>=V[i]&&d[S]==d[S-V[i]]+1){
			printf("%d ",i);
			printf_ans(d,S-V[i]);
			break;
		}
}
  1. 另一种打印路径的方法:在递推时直接用min_coin[S]记录满足min[S]==min[S-V[i]+1]的最小的i,则打印路径时可以省去print_ans中的循环,使用空间换时间
for(int i=1;i<=S;i++)
	for(int j=1;j<=n;j++)
		if(i>=V[j]){
			if(min[i]>min[i-V[j]]+1){
				min[i]=min[i-V[j]]+1;
				min_coin[i]=j;
			}
			if(max[i]<max[i-V[j]]+1){
				max[i]=max[i-V[j]]+1;
				max_coin[i]=j;
			}
		}

void print_ans(int*d,int S){
	while(S){
		printf("%d",d[S]);
		S -= V[d[S]];
	}
}

0-1背包问题

物品无限的背包问题

有n种物品,每种都有无穷多个,第i种物品的体积为Vi,重量为Wi。选一些物品装到一个容器为C的背包中,使得背包内的物品在总体积不超过C的前提下重量尽可能大。 1 &lt; = n &lt; = 100 , 1 &lt; = V i &lt; = C &lt; = 10000 , 1 &lt; = W i &lt; = 1 0 6 1&lt;=n&lt;=100,1&lt;=Vi&lt;=C&lt;=10000,1&lt;=Wi&lt;=10^6

分析可知,是在有向无环图的基础上增加了权值,+1变成了+W[i]。

0-1背包问题

当物体只有一个时,称为0-1背包,仅仅考虑体积,无法得知每个物品是否已经用过。需要对问题进行分层:用d(i,j)表示当前在第i层,背包剩余容量为j时接下来的最大重量和,则 d ( i , j ) = m a x [ d ( i + 1 , j ) , d ( i + 1 , j V [ i ] ) + W [ i ] ] d(i,j)=max[d(i+1,j),d(i+1,j-V[i])+W[i]] ,d(i,j)表示把第i,i+1,i+2…n个物品装到容量为j的背包中的最大总重量。分层后,采用递推法更好:

  1. 如下最终答案为d[1][C]:
for(int i=n;i>=1;i--)
	for(int j=0;j<=C;j++){
		d[i][j]=(i==n ? 0 : d[i+1][j]);
		if(j>=V[i])
			d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
	}
  1. 还可采取另一种状态定义 f ( i , j ) = m a x [ f ( i 1 , j ) , f ( i 1 , j V [ i ] ) + W [ i ] ] f(i,j)=max[f(i-1,j),f(i-1,j-V[i])+W[i]] ,最终答案为f(n,C):
for(int i=1;i<=n;i++)
	for(int j=0;j<=C;j++){
		f[i][j]=(i==1?0:f[i-1][j]);
		if(j>=V[i])
			f[i][j]=max(f[i][j],f[i-1][j-V[i]]+W[i]);
	}

这种定义运行边读边计算:

for(int i=1;i<=n;i++){
	scanf("%d%d",&V,&W);
	for(int j=0;j<=C;j++){
		f[i][j]=(i==1?0:f[i-1][j]);
		if(j>=V)
			f[i][j]=max(f[i][j],f[i-1][j-V]+W);
	}
}

小记

  1. 用DFS求连通域
  2. 用BFS求最短路
  3. 拓扑排序:DFS求拓扑排序,成功则为有向无环,否则有环。
  4. vector保存结点避免溢出
  5. 回溯法(剪枝):无法继续扩展,阶数递归,返回上一层
for(...)
	if(...)
		OK=0;break;
	if(OK)
		dfs(递归);
  1. cstring的memcmp和memcpy复制比循环快
  2. 树、图的DFS、BFS判重技术:哈希技术,将结点映射为整数
  3. 图DFS检查idx,BFS检查d值是否为-1,以此来判断是否访问过
  4. scanf不安全问题
#define _CRT_SECURE_NO_WARINGS
  1. 消防车问题:
if(src==dest)
	//找到目标
else
	for(遍历下一个结点)
		if(判重)
			continue;
		dfs(递归);

猜你喜欢

转载自blog.csdn.net/zhangquan2015/article/details/82915889
今日推荐