啊我终于要开始coding了,作为小菜鸡自然不能跟ACM大神们卷互联网大厂,那么我就乖乖的飞回我的小青岛跟小哥哥幸福快乐的生活叭哈哈哈~~
银行国企等科技岗对编程要求比较基础,因此本博客将总结基础类型编程题目~
三、各种图问题
1、图的遍历,深度优先遍历DFS以及广度优先遍历BFS,时间复杂度差不多,但搜索思路截然不同。DFS类似树的先序遍历,BFS类似树的层次遍历。有时候不出现图也可以使用搜索解决问题,往下看叭
例1.全排列与DFS,起初在学习使用DFS的方式解决全排列问题时,我以为是将所有结果构建成图然后可以遍历得到所有的结果,但实际上与构建图并没有关系。只是使用递归的方式解决全排列问题时的思想(搜索到底之后回溯)与DFS相似。
如输出数字、字母的全排列。将扑克牌1-3放入1-3号盒子中,每个盒子只能有一张。人为规定先放小的牌,首先1-1,2-2,3-3,完成之后,将3号牌收回,但是没有别的选择,再回溯到2号盒子,收回2号牌,此时可以做另一种选择,3-2,2-3.以此类推1,2,3/1,3,2/2,1,3/ 2,3,1/3,1,2/3,2,1
P.S. memset 函数是内存赋值函数,用来给某一块内存空间进行赋值的,在头文件string.h中
memset
( &a, 0,
sizeof
(a) );
//注意第一个参数是指针类型,a不是指针变量,要加&
memset
( b, 0,
sizeof
(b) );
//b是数组名,就是指针类型,不需要加&
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
int len;
string str;
string ans;
bool used[1000];
void dfs(int boks)
{
if(boks==len)
{
cout<<ans<<endl;
return;
}
for(int i=0;i<len;i++)
{
if(used[i]==false)
{
ans[boks]= str[i]; //将数字/字母放入箱子中
used[i]=true;
dfs(boks+1); //进行下一个箱子的放置
used[i]=false;
}
}
}
int main()
{
while(cin>>str)
{
sort(str.begin(),str.end());
len = str.length();
memset(used,false,sizeof(used)); //初始化使用矩阵为false
ans = str; //在修改时是逐位修改的,所以给字符串赋一个初值,不然空串没办法在相应位置修改
dfs(0);
cout<<endl;
}
}
例2. 八皇后问题,递归回溯
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将8个皇后放在棋盘上(有8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。 对于某个满足要求的8皇后的摆放方法,定义一个皇后串a与之对应,即a=b1b2...b8,其中bi为相应摆法中第i行皇后所处的列数。已经知道8皇后问题一共有92组解(即92个不同的皇后串)。 给出一个数b,要求输出第b个串。串的比较是这样的:皇后串x置于皇后串y之前,当且仅当将x视为整数时比y小。
#include<iostream>
using namespace std;
string str[1000]; //保存所有的排列方式
int count = 0; //记录当前是第几种排列
int place[1000] = {0}; //保存每一行放置位置的列号
void puts(int boks)
{
if(boks==8) //说明所有的列都已经放置完成了
{
char temp;
for(int i = 0;i<8;i++) //将本次排列输出
{
temp = '1'+place[i];
str[count]+=temp;
}
count++;
}
else{
for(int i=0;i<8;i++)
{
place[boks] = i; //为第boks行放置
int flag = 1;
for(int j = 0;j<boks;j++) //(boks,i),(j,place[j]) //判断与之前的行是否存在冲突,主要包含列号相同或者在左右对角线上
{
if(i==place[j]||boks-i==j-place[j]||boks+i==j+place[j])
{
flag = 0;
break;
}
}
if(flag)
{
puts(boks+1); //进行下一行放置
}
}
}
}
int main()
{
int n;
puts(0);
while(cin>>n){
cout<<str[n-1]<<endl;
}
return 0;
}
例3. BFS
P.S. string中find()返回值是字母在母串中的位置(下标记录),如果没有找到,那么会返回一个特别的标记npos。(返回值可以看成是一个int型的数)
玛雅人有一种密码,如果字符串中出现连续的2012四个数字就能解开密码。给一个长度为N的字符串,(2=<N<=13)该字符串中只含有0,1,2三种数字,问这个字符串要移位几次才能解开密码,每次只能移动相邻的两个数字。例如02120经过一次移位,可以得到20120,01220,02210,02102,其中20120符合要求,因此输出为1.如果无论移位多少次都解不开密码,输出-1。
每次移动一位得到一个新的字符串就认为是一个新的树节点。
#include<iostream>
#include<queue>
#include<map>
using namespace std;
map<string, int>m; //用map记录字符串以及其对应的移动次数
queue<string>q; //利用队列实现广度优先遍历
bool judge(string s)
{
if(s.find("2012")!=string::npos) //字符串搜索子串,如果找到则返回位置下标,否则返回npos
{
return true;
}
else
return false;
}
string swap(string s,int i)
{
char temp;
temp = s[i];
s[i] = s[i+1];
s[i+1] = temp;
return s;
}
int bfs(string s)
{
while(!q.empty())q.pop();
m.clear();
q.push(s);
m[s] = 0;
while(!q.empty())
{
s = q.front();
q.pop();
for(int i = 0;i<s.length();i++)
{
string str1 = swap(s,i);
if(m.find(str1)==m.end()) //在map中寻找是否包含有键值,如果没有则返回end,有则返回下标
{
m[str1] = m[s]+1;
if(judge(str1))
return m[str1];
else
q.push(str1);
}
else
continue;
}
}
return -1;
}
int main()
{
int n;
string s;
while(cin>>n>>s)
{
if(judge(s))
cout<<0<<endl;
else
cout<<bfs(s)<<endl;
}
return 0;
}
2. 图的最小生成树
克鲁斯卡尔算法以及普利姆算法均利用了贪心的思想,算法1从不同连通分支中选择权值小的边将其加入边集合中,将不同连通分支的点合并,最终形成一个连通分支;算法2从点出发,选择从该点出发的最小权值的边,直到所有点均与边相连。
克鲁斯卡尔算法用到了并查集,所谓并查集是一种用于管理分组的数据结构。它具备两个操作:(1)查询元素a和元素b是否为同一组 (2) 将元素a和b合并为同一组。其不能将分组拆分。
int node[i]; //每个节点
//初始化n个节点,每个节点的根节点都是自身,相当于每个节点为一个组
void Init(int n){
for(int i = 0; i < n; i++){
node[i] = i;
}
}
//查找当前元素所在树的根节点(代表元素),递归的方式查找
int find(int x){
if(x == node[x])
return x;
return find(node[x]);
}
//合并元素x,y所处的集合
void Unite(int x, int y){
//查找到x,y的根节点
x = find(x);
y = find(y);
if(x == y)
return ;
//将x的根节点与y的根节点相连,将y连在x的根节点上
node[x] = y;
}
//判断x,y是属于同一个集合
bool same(int x, int y){
return find(x) == find(y);
}
注意,若每次均简单的将一个节点连接到另一个节点的根节点上容易造成树的退化(成线性的),导致并查集算法的复杂度非常高。因此有如下方法进行并查集算法的优化:1. 对于每一棵树,记录其高度(rank)。在每次合并操作时,将高度小的树放在高度高的树下,成为其子树;2. 路径压缩,即将原先间接与根相连的节点,让其与根直连,从而提高效率。
并查集举例:
例1:
#include<iostream>
#include<string.h>
using namespace std;
int root[100];
int getindex(int x,int y) //寻找根节点,如果最终找到y的根是x,说明两者之间是直系亲属,否则无关
{
int count = 1;
while(y!=-1)
{
if(root[y]==x)
return count;
else
{
y = root[y];
count++;
}
}
return -1;
}
int main()
{
int n,m;
while(cin>>n>>m)
{
string s;
memset(root,-1,sizeof(root));
while(n--)
{
cin>>s;
for(int i=1;i<s.size();i++)
{
if(s[i]!='-')
root[s[i]-'A'] = s[0]-'A'; //父母的根节点为孩子
}
}
char a,b;
while(m--)
{
cin>>a>>b;
int index1 = getindex(a-'A',b-'A'); //不知道询问的顺序,可能是孩子也可能是父母
//分情况讨论
int index2 = getindex(b-'A',a-'A');
if(index1==-1&&index2==-1)
{
cout<<'-'<<endl;
}
else if(index1!=-1)
{
if(index1==1)cout<<"child"<<endl;
else if(index1==2)cout<<"grandchild"<<endl;
else
{
for(int i=2;i<index1;i++)
{
cout<<"great-";
}
cout<<"grandchild"<<endl;
}
}
else if(index2!=-1)
{
if(index2==1)cout<<"parent"<<endl;
else if(index2==2)cout<<"grandparent"<<endl;
else
{
for(int i=2;i<index2;i++)
{
cout<<"great-";
}
cout<<"grandparent"<<endl;
}
}
}
}
}
例2:
//树的条件:首先只有一个根以及一个连通分支(并查集),节点只能有一个入度
#include<iostream>
#include<stdio.h>
using namespace std;
int pre[10001];
int in[10001];
bool vis[10001];
void init()
{
for(int i=1;i<=10000;i++)
{
pre[i]=-1;
in[i]=0;
vis[i]=false;
}
}
int find(int x)
{
if(pre[x]==-1)return x;
else return find(pre[x]);
}
int main()
{
int cas=0;
int a,b;
while(cin>>a>>b)
{
if(a<0&&b<0)break;
if(a==0&&b==0)//最开始的时候为0,0也是一棵树。空树
{
printf("Case %d is a tree.\n",++cas);
continue;
}
init();
vis[a]=true;
vis[b]=true;
in[b]++;
int x=find(a);
int y=find(b);
if(x!=y)pre[a]=b;
while(cin>>a>>b)
{
if(a==0&&b==0)break;
vis[a]=true;
vis[b]=true;
in[b]++;
int x=find(a);
int y=find(b);
if(x!=y)pre[a]=b;
}
bool anser=true;
int root=0;
int component=0;
for(int i=1;i<=10000;i++)
{
if(vis[i]&&in[i]==0)root++;
if(in[i]>=2)anser=false;
if(vis[i]&&pre[i]==-1)component++;
}
if(root!=1)anser=false;
if(component!=1)anser=false;
if(anser==true) printf("Case %d is a tree.\n",++cas);
else printf("Case %d is not a tree.\n",++cas);
}
}
例3. 利用并查集得到图的连通分支
#include<iostream>
#include<string.h>
using namespace std;
int pre[1001];
int find(int x) //并查集代码
{
if(pre[x]==-1)return x;
else return find(pre[x]);
}
int main()
{
int n,m;
int a,b;//一条边的起点以及终点
while(cin>>n>>m&&n!=0)
{
if(m==0)
{
cout<<n-1<<endl;
continue;
}
memset(pre,-1,sizeof(pre));
for(int i=0;i<m;i++)
{
cin>>a>>b;
int x=find(a);
int y=find(b);
if(x!=y)
{
pre[x]=y;
}
}
int ans=0;
for(int i=1;i<=n;i++)
{
if(pre[i]==-1)ans++;
}
cout<<ans-1<<endl;
}
return 0;
}
例1. 克鲁斯卡尔算法举例
In an episode of the Dick Van Dyke show, little Richie connects the freckles on his Dad's back to form a picture of the Liberty Bell. Alas, one of the freckles turns out to be a scar, so his Ripley's engagement falls through. Consider Dick's back to be a plane with freckles at various (x,y) locations. Your job is to tell Richie how to connect the dots so as to minimize the amount of ink used. Richie connects the dots by drawing straight lines between pairs, possibly lifting the pen between lines. When Richie is done there must be a sequence of connected lines from any freckle to any other freckle.
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<math.h>
using namespace std;
struct node //定义节点,包含x,y坐标
{
double x;
double y;
}nodes[1000];
struct edge{ //定义边,包含起点编号,终点编号以及两点之间距离(权值)
int start;
int end;
double cost;
}edges[6000];
int pre[1000]; //存节点的根节点
double dis(node a,node b)
{
return sqrt(pow((a.x-b.x),2)+pow((a.y-b.y),2)); //计算权值/距离
}
bool cmp(edge a, edge b) //C++中sort函数可以自己定义比较方式,即构造cmp函数
{
return a.cost<b.cost;
}
int find(int a){ //寻找某节点的根节点
if(pre[a]==a)return a;
else return find(pre[a]);
}
int main()
{
int n;
while(cin>>n)
{
for(int i=0;i<n;i++)
{
cin>>nodes[i].x>>nodes[i].y;
}
int size = 0;
for(int i=0;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
edges[size].start = i;
edges[size].end = j;
edges[size++].cost = dis(nodes[i],nodes[j]);
}
} //构造完全图
sort(edges,edges+size,cmp); //根据权值由小到大排列边
double ans = 0;
for(int i=0;i<n;i++)
{
pre[i] = i;
}
for(int i=0;i<size;i++)
{
int t1 = find(edges[i].start); //如果边的两个顶点不在一个连通分支,则加入边
int t2 = find(edges[i].end);
if(t1!=t2)
{
ans+=edges[i].cost;
pre[t1] = t2; //将两个顶点所在的连通分支合并
}
}
cout<<ans<<endl;
}
}
例2. 畅通工程
//克鲁斯卡尔算法求最小生成树,因为该题是将全部村庄连通的最小花费。
//最短路径是不管是否连通的,其求的是从源节点到目的节点的开销最小
#include<iostream>
#include<algorithm>
using namespace std;
struct edge{
int start,end;
int cost;
};
edge e[101];
int pre[101];
bool cmp(edge a, edge b)
{
return a.cost<b.cost;
}
int find(int a)
{
if(pre[a]==-1)return a;
else return find(pre[a]);
}
int main()
{
int n,m;
while(cin>>n>>m)
{
if(n==0)break;
for(int i =1;i<=m;i++)
{
pre[i] = -1;
}
for(int i=0;i<n;i++)
{
cin>>e[i].start>>e[i].end>>e[i].cost;
}
sort(e,e+n,cmp);
int ans = 0;
for(int i=0;i<n;i++)
{
int a = find(e[i].start);
int b = find(e[i].end);
int c = e[i].cost;
if(a!=b)
{
pre[a]=b;
ans+=c;
}
}
int root = 0;
for(int i=1;i<=m;i++)
{
if(pre[i]==-1)root++;
}
if(root>1)cout<<'?'<<endl;
else cout<<ans<<endl;
}
}
例3.继续畅通工程
该题目与畅通工程的区别为,已见道路的选择优先级高于未建道路,都是未建道路则开销小的道路优先选择。
#include<iostream>
#include<algorithm>
using namespace std;
struct edge{
int finish;
int start;
int end;
int cost;
};
int pre[101];
bool cmp(edge a, edge b)
{
if(a.finish==1&&b.finish==1||a.finish==0&&b.finish==0)
//表示只有当路径都是未完成时才比较开销
return a.cost<b.cost;
else return a.finish>b.finish;
}
int find(int a)
{
if(pre[a]==-1)return a;
else return find(pre[a]);
}
int main()
{
int n;
while(cin>>n){
if(n==0)break;
for(int i=1;i<=n;i++)
pre[i] = -1;
int m = n*(n-1)/2;
edge e[10001];
for(int i = 0;i<m;i++)
{
cin>>e[i].start>>e[i].end>>e[i].cost>>e[i].finish;
}
sort(e,e+m,cmp);
int ans = 0;
for(int i=0;i<m;i++)
{
int a = find(e[i].start);
int b = find(e[i].end);
if(a!=b){
pre[a] = b;
if(e[i].finish==0)
ans+=e[i].cost;
}
}
cout<<ans<<endl;
}
return 0;
}
例4.
题目大意就是一个部落有n个村子,若干条道路,每个道路有它的花费,要修一条可以连接所有村子的路,保证花费最小,典型的最小生成树。
用例第一行是n个村子,之后的n-1行是道路信息
1 |
|
代表有两条路通往村子A,AB之间的花费是12,AI之间的花费是25。
#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
struct edge{
int start;
int end;
double cost;
}edges[100];
int pre[100];
bool cmp(edge a, edge b)
{
return a.cost<b.cost;
}
int find(int a)
{
if(pre[a]==a)return a;
else return find(pre[a]);
}
int main()
{
int n;
while(cin>>n)
{
int size = 0;
for(int i=0;i<n-1;i++)
{
char c;
int num;
cin>>c;
cin>>num;
for(int j = 0;j<num;j++){
char e;
int cost;
cin>>e>>cost;
edges[size].start = c-'A';
edges[size].end = e-'A';
edges[size++].cost = cost;
}
}
sort(edges,edges+size,cmp);
double ans = 0;
for(int i=0;i<100;i++)
{
pre[i]=i;
}
for(int i = 0;i<size;i++)
{
int t1 = find(edges[i].start);
int t2 = find(edges[i].end);
if(t1!=t2)
{
ans+= edges[i].cost;
pre[t1]=t2;
}
}
cout<<ans<<endl;
}
}
例5:
该题目中所有公司同时开工,则修建整条地铁需要的最少的天数为所有路径中最长边的最小值。由于是同时开工的,公司并行修建不同的隧道,因此需要的天数不需要相加,而是求最长边的长度即为修建完成的天数,完成天数的最小值即为不同路径中最长边的最小值。
#include<iostream>
#include<algorithm>
#include<string.h>
#include<vector>
using namespace std;
struct edge
{
int a,b,cost;
};
bool cmp(edge a,edge b)
{
return a.cost<b.cost;
} //按照边的权值,由小到大排列
vector<edge>edges;
int pre[100010];
int find(int x)
{
if(pre[x]==-1)return x;
else return find(pre[x]);
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
edge n;
cin>>n.a>>n.b>>n.cost;//将边输入
edges.push_back(n);
}
sort(edges.begin(),edges.end(),cmp);
int ans;
for(int i=1;i<=n;i++)
{
pre[i]=-1;
}
for(int i=0;i<m;i++)
{
int x=find(edges[i].a);
int y=find(edges[i].b);
ans=edges[i].cost;
if(x!=y)
{
pre[x]=y;
}
if(find(1)==find(n))
{
break;
}
}
cout<<ans<<endl; //按照边从小到大看是否1与n是一个连通分支,使得1与n成为一个连通分支的边即为所求。
return 0;
}
3. 图的最短路径
求最短路径的算法:迪杰特斯拉算法、弗洛伊德算法
最小生成树能够保证整个图的所有路径之和最小(所有节点都遍历了),但不能保证任意两点之间是最短路径。
最短路径是从一点出发,到达目的地的路径最小。(有源节点以及目的节点,两点之间距离最小)
遇到求所有路径之和最小的问题用最小生成树&并查集解决;遇到求两点间最短路径问题的用最短路,即从一个城市到另一个城市最短的路径问题。
最小生成树构成后所有的点都被连通(节点均被遍历),而最短路只要到达目的地走的是最短的路径即可,与所有的点连不连通没有关系。
迪杰特斯拉算法(本质为贪心)是单源最短路径算法,不能解决负权图(起点到终点的最短路径);弗洛伊德算法是多源最短路径(每一对节点之间的最短路径)
例1.
//该问题实际上为迪杰特斯拉的变形,当权值相同的时候看开销,选开销小的
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct E{ //定义边,包括边相连的后一个顶点,边的权值,花费
int next;
int cost;
int distance;
};
vector<E> edges[1001]; //定义vector数组,节点及其相连的边
bool visited[1001];//该节点是否已经在最短路径节点集合中
int Dis[1001], Cos[1001];
int main()
{
int n,m;
while(cin>>n>>m)
{
if(n==0&&m==0)break;
for(int i=1;i<=n;i++) //初始化
{
Dis[i] = -1;
Cos[i] = -1;
visited[i] = false;
edges[i].clear(); //edges[i]是一个vector
}
int a,b,dis,cos;
for(int i = 0;i<m;i++)
{
E n;
cin>>a>>b>>dis>>cos;
n.distance = dis;
n.cost = cos;
n.next = a;
edges[b].push_back(n);
n.next = b;
edges[a].push_back(n);
}//构建无向图,每个节点维护其相连的边
//进行两层循环,外层循环进行n-1次,从第一个节点到其他n-1个节点
//内层循环进行边松弛
int start,end,newp;
cin>>start>>end;
newp = start;
Dis[newp]= 0;
Cos[newp] = 0;
for(int i = 1;i < n;i++)
{
for(int j = 0;j<edges[newp].size();j++) //初始点的所有边
{
int n = edges[newp][j].next;
int c = edges[newp][j].cost;
int d = edges[newp][j].distance;
if(visited[n]==true)continue;
if(Dis[n]==-1||Dis[n]>Dis[newp]+d||Dis[n]==Dis[newp]+d&&Cos[n]>Cos[newp]+c)
{
Dis[n] = Dis[newp]+d;
Cos[n] = Cos[newp]+c;
}
}//将节点newp所连的所有边松弛一遍
//接下来找到最小的边,加入visited集合中
int min = 9999;
for(int i = 1;i<=n;i++)
{
if(Dis[i]==-1)continue;
if(visited[i]==true)continue;
if(Dis[i]<min)
{
min = Dis[i];
newp = i;
}
}
visited[newp] = true;
}
cout<<Dis[end]<<' '<<Cos[end]<<endl;
}
return 0;
}
例2.
此题是变形的迪杰特斯拉算法,最短路径中只能有一条支持不同领导者的路径。题目中提到起点为1城市,终点为2城市,且两者支持不同领导。因此其他路径不能支持不同领导者。
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct E{
int next;
int cost;
};
vector<E>edges[10001];
bool marked[10001];
int cost[10001];
int leader[10001];
int main()
{
int n,m;
while(cin>>n)
{
if(n==0)break;
cin>>m;
for(int i=1;i<=n;i++)
{
cost[i]=-1;
marked[i] = false;
edges[i].clear();
}
int a,b,cos;
E e;
for(int i=1;i<=m;i++)
{
cin>>a>>b>>cos;
e.cost = cos;
e.next = a;
edges[b].push_back(e);
e.next = b;
edges[a].push_back(e);
}
for(int i=1;i<=n;i++)
{
cin>>leader[i];
}
int start = 1;
int end = 2;
int newp = 1;
cost[newp]=0;
marked[newp] = true;
for(int i=1;i<n;i++)
{
for(int j=0;j<edges[newp].size();j++)
{
int n = edges[newp][j].next;
int c = edges[newp][j].cost;
if(marked[n]==true)continue;
if(leader[n]==1&&leader[newp]==2)continue; // 只能从1到2,不能从2营到1营
if(cost[n]==-1||cost[n]>cost[newp]+c)
{
cost[n] = cost[newp]+c;
}
}
int min = 99999;
for(int i=1;i<=n;i++)
{
if(cost[i]==-1)continue;
if(marked[i]==true)continue;
if(min>cost[i])
{
min = cost[i];
newp = i;
}
}
marked[newp] = true;
}
cout<<cost[2]<<endl;
}
return 0;
}