题解前话:题目据说大多出自leetcode,而对于大多数计算机系生如果有一定leetcode基础会对招聘有加分,所以各位要重视这些题。
然后,我不是图论选手,我的图论知识比较贫乏,有的题最优解我更倾向于简单算法。
开始正文,先讲一下输入问题,题目里有‘,’这样的输入方式,是比较特别的,你可以用scanf的性质把‘,’读取。这里详情可以了解以下scanf函数:转载别人的博客
最短路径
我选择的是Floyd,但是这题时间最优应该是SPFA,效率适中我会考虑dijistra(迪杰斯特拉),由于能力有限我就讲一下Floyd,有兴趣各位可以自己补。
Floyd(弗洛伊德)的本质就是对于所有 i i i点,枚举出所有边 j − k j-k j−k与其相连,判定这个 j − k j-k j−k线路是否存在经过了 i i i点可以提高效率。因此Floyd首先要做的事情是初始化,将点与点孤立,也就是边无穷大。然后将联通的点和点利用邻接矩阵存储距离。
这里解释一下邻接矩阵,是一个存图的常用算法。对于表示 a i j a_{ij} aij我们存储 i − j i-j i−j的路径(长度)。
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
int n;
double a[200][200];
const int inf = 0x3f3f3f3f;
//重点的三个函数(核心部分)
int main() {
int m;
int x, y;
int flag = 1;
cin >> n;
cin >> m;
cin >> x >> y;
//以上为输入情况
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
a[i][j] = inf;//固定无穷大常数
}
}
for (int i = 1; i <= m; i++) {
int t, r;
scanf("%d %d", &t, &r);
scanf("%lf", &a[t][r]);
//a[t][r] = 1;
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++)
{
//如果发现经过i点的两条边相加之和小于这两个点之间的距离,那么更新数据
if (a[j][k] > a[j][i] + a[i][k])
a[j][k] = a[j][i] + a[i][k];
}
}
}
if (a[x][y]<=inf)printf("%.2f", a[x][y]);
else cout << "false" << endl;
}
课程排序(刚才锅了,假算法)
刚预习了一遍拓扑排序就来写题解我也是心大。
首先拓扑排序的本质就是从每个入度为零的地方起步,然后目标遍历所有点。
注释1:入度,代表某点进入该点的次数。
那么如果不从入度为0的地方起步可以么,按照题意来说应该不可以。因此只需要找到所有的入度为零的地方,找找他能走到哪,把那些到过的点标记,计算一共标记了几次点。
解法1:暴力遍历:
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 1000;
int T[maxn][maxn];//标记图
int D[maxn];//标记顶点的入度
int n;//顶点个数
int m;//边的个数
int arr[maxn];//记录排序后的顺序
int num = 0;
int topSort()
{
for (int i = 0; i < n; i++)//得到每个节点
{
int j;
for (j = 1; j <= n; j++)//遍历找到入度为0的结点
{
if (D[j] == 0)
{
arr[num++] = j;//标记到arr数组中
D[j]--;//标记已使用
break;
}
}
for (int k = 1; k <= n; k++)//遍历所有边
{
if (T[j][k] == 1)//找到以j开头的边
{
T[j][k]--;//去掉该边
D[k]--;//k顶点入度减一
}
}
}
return 0;
}
int main()
{
int x, y;
memset(T, 0, sizeof(T));
memset(D, 0, sizeof(D));
cin >> n;
cin >> m;
for (int i = 0; i < m; i++)
{
scanf("%d,%d", &x, &y);
T[x][y] = 1;//标记边
D[y]++;//记录入度
}
topSort();
if (num == n)
{
cout << "true\n";
}
else
{
cout << "false" << endl;
}
return 0;
}
有句话说的好,只要循环次数够多,再怎么难的NP难题也可以解开,因此这个暴力做法就是利用大量的循环访问实现枚举效果,做到算无遗策。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 9;
int ind[maxn], n, m;
queue<int> q;
vector<int> g[maxn];
int main() {
scanf("%d%d", &n, &m);
while (m--) {
int u, v;
scanf("%d,%d", &u, &v);
ind[v]++;
g[u].push_back(v); //可以理解为二维数组
}
for (int i = 0; i < n; i++)
if (ind[i] == 0) q.push(i); //把入度为0的结果放入队列
while (q.size()) { //如果队列不为空
int v = q.front(); //把队列的头读出来执行走路模拟
q.pop(); //出列
for (int i : g[v]) //遍历
if (--ind[i] == 0) q.push(i);//入列,减少入度
}
for (int i = 0; i < n; i++)
if (ind[i]) {
puts("false");//存在入度不为0
return 0;
}
puts("true");
}
这个做法利用了BFS,也就是将你会执行的步骤压在队列的最下面,从上往下有序执行,实现搜索每一个点。
并查集应用:判圈
课程排序
我的做法是并查集找联通情况,通过判断连线的两个点是否存在同一个祖先,来判定是否成环。
我们先从并查集开始讲,并查集其实是三个函数组成的。分别是,初始化,查找祖先,判定是否有共同祖先,如果不是,把他们俩连起来挂在一个祖先名下。
初始化:让每个点的祖先都是自己。
查找祖先:写一个递归函数,判定自己的上一级是谁,让上一级去追查上一级,层层询问,问到某个人是自己的祖先停止,然后告诉下面的人,你们的祖先是这个,下次就不用问多次询问上级了。
打架:现在有两个人他们相遇了,要打一架,但他们怕自己打了自家人,就问问上级这个是不是自己人,如果是,那就握手言和(成环了),如果不是,这两个人不打不相识,把自己拜入对方家谱中。
如果还是不太懂可以看这篇优快云,我当初也是看这个自学的。
转载:并查集(他也是转载)
这里代码贴个板子给大家学习一下,板子算是我压缩用来当黑箱用的,所以比较短:
const int N = 1001;
//重点的三个函数(核心部分)
int fa[N];
void init() { //初始化
for (int i = 0; i < N; i++)
fa[i] = i;
}
int findfa(int x) { //找到某个数x的祖先
return x == fa[x] ? x : (fa[x] = findfa(fa[x]));
}
void Union(int x, int y) { //把x和y合并(二者之间连一条线)
int fx = findfa(x), fy = findfa(y);
if (fx != fy) fa[fx] = fy; //注意是fa[fx]=fy, 而不是fa[x]=fy; 当然了fa[fy]=fx也行
}
所以这题也就用到了这个思想,如果成环就不能排序,那么就判定是否有成环的情况就可以了。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int x[2000];
const int N = 1001;
int fa[N];
int r[N][N];
void init() { //初始化
for (int i = 0; i < N; i++)
fa[i] = i;
}
int findfa(int x) { //找到某个数x的祖先
return x == fa[x] ? x : (fa[x] = findfa(fa[x]));
}
void Union(int x, int y) { //把x和y合并(二者之间连一条线)
int fx = findfa(x), fy = findfa(y);
if (fx != fy) fa[fx] = fy; //注意是fa[fx]=fy, 而不是fa[x]=fy; 当然了fa[fy]=fx也行
}
int main() {
init();
int a, b, c;
cin >> a >> b >> c;
for (int i = 0; i < b; i++) {
int n, m;
cin >> n >> m;
r[n][m] = 1;
r[m][n] = 1;
Union(n, m);//连线
}
for (int i = 0; i < c; i++) {
int n, m;
cin >> n >> m;//判定是否存在连线或者成环
if (r[n][m] == 1)cout << "YES" << endl;
else if (findfa(n) == findfa(m))cout << "YES" << endl;
else cout << "NO" << endl;
}
}
省份数量
强连通分量判定,最熟悉的算法是缩点,也就是离散化,为了给大家巩固一下并查集算法,我就继续用并查集讲了。
已知无向图可以转化为有向图,也就可以转化为并查集寻找 祖先为自身 的个数。
为什么呢,我们继续打架,每两个城市相连,代表他们要打一次架。这时候我们心中存一群母树,每个点独立一棵,每次两个人打架就把这两个点连起来,固定一个点为母树(作为祖先),然后每次打架都让他们问问他们的上级谁是领导,把这些人都挂在领导的名下,这些独立的点逐渐就挂在了联通的几棵树上了,那么母树的数量就成了联通的量了。
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
#define MAX_N 10000
vector<long long > g[MAX_N];
int color[MAX_N];
const int N = 1001;
//重点的三个函数(核心部分)
int fa[N];
void init() { //初始化
for (int i = 0; i < N; i++)
fa[i] = i;
}
int findfa(int x) { //找到某个数x的祖先
return x == fa[x] ? x : (fa[x] = findfa(fa[x]));
}
void Union(int x, int y) { //把x和y合并(二者之间连一条线)
int fx = findfa(x), fy = findfa(y);
if (fx != fy) fa[fx] = fy; //注意是fa[fx]=fy, 而不是fa[x]=fy; 当然了fa[fy]=fx也行
}
int a[200][200];
int main() {
int n;
cin >> n;
init();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n-1; j++) {
scanf("%d," ,& a[i][j]);
}
scanf("%d", &a[i][n]);
// cout << a[i][n - 1];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j < n; j++) {
if (a[i][j] == 1) {
Union(i, j);//找关系打架
}
}
}
int ans = 0;//数祖先是自己的个数
for (int i = 1; i <= n; i++) {
if (fa[i] == i)ans++;
}
cout << ans << endl;
}
二分图判断
这题有一个结论:二分图成立的前提是他们的环为偶数,然后可以理解为,每相邻两个点颜色不同(你可以把颜色不同理解为在不同的集合,二分图一共只有两个集合)。
因此这题可以用DFS染色做,也可以BFS染色做,终归就是染色出结果。
这里介绍BFS染色法:
从任意点出发,对于周围所有可以走的路都放入队列中,然后将自己当前站的位置染成一种颜色,对于下一次要走的路染成另一种颜色(这里可以用!运算来实现),因为队列本身的性质,必须要把当前所有的路都走完才能走下一次的路,因此,实现上用迭代的做法。
以下为我的染色代码:
int bfs_check(int x) {//判断是否为二分图
queue<int> q;
q.push(x);
color[x] = 1;
while (!q.empty()) {
int from = q.front();
q.pop();
int sz = g[from].size();
for (int i = 0; i < sz; i++) {
if (color[g[from][i]] == -1) {
q.push(g[from][i]);
color[g[from][i]] = !color[from];//相邻的点染成不同颜色
}
if (color[g[from][i]] == color[from]) {//相邻的点颜色相同
return 0;//不是
}
}
}
return 1;//是
}
这个不贴完整代码,因为用的是vector存图,你们不方便改。
数据流的中位数
Leetcode 数据结构题
反正我喜欢用优先队列,那我也就讲优先队列的做法,具体实现大家可以自己摸索。
首先,c++库里有优先队列的成品,我就不另起炉灶,直接讲大小堆实现中位数判定。
一个堆存1 2 3 4 5 另一个堆存10 9 8 7 6。根据优先队列的特殊性质,你可以将他们输进去就排序,那我就设计了一个小堆装升序的小数字,一个大堆装降序的大数字。如果两个堆数据总和是奇数,那么输出多一点的堆的队列顶端,如果是偶数,就把两个队顶拿来平均以下。
最后解释一下这个大小堆的维护,空队列优先丢空队列,比小堆的最大值大就丢大堆,如果比大堆的最小值小就丢小堆。
大堆一边比小堆一边大1或两边size相等(这里也可以小堆比大堆,只要一致保持就可以了) 一直用if去判定就好了。
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#include<cstring>
using namespace std;
#define MAX_N 10000
vector<long long > g[MAX_N];
int color[MAX_N];
priority_queue<long long >Big;
priority_queue<long long , vector<long long >, greater<long long > >Small;
void addNum(long long num) {
if (Small.empty() && Big.empty()) Big.push(num);
else if (Small.empty()) Big.push(num);
else if (Big.empty()) Small.push(num);
else if (num <= Small.top()) Big.push(num);
else Small.push(num);
if (Small.size() > Big.size()) {
Big.push(Small.top());
Small.pop();
}
if (Big.size() - Small.size() > 1) {
Small.push(Big.top());
Big.pop();
}
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) {
long long a;
cin >> a;
addNum(a);
int sum = Small.size() + Big.size();
if (sum & 1)
printf("%.1f\n", (double)Big.top());
else
printf("%.1f\n",1.0 * (Small.top() + Big.top()) / 2);
}
}