【蓝桥杯】每日练习 Day15
目录
前言
奶牛选美
分析
代码
大臣的旅费
分析
代码
飞机降落
分析
代码
母亲的牛奶
分析
代码
扫雷
分析
代码
前言
虽为诞辰,但也不忘完成每日的训练。
今天给大家带来五道dfs
的题目,包括组合数,连通块,数的直径等方面的内容。
因为时间较为仓促,很多地方可能讲的不是很全面,请见谅。
奶牛选美
分析
题目保证图中存在且仅存在两个连通块,说实话这道题挺水的,不知道为什么难度是中等。
第一步,dfs
或bfs
找出两个连通块。
随后的问题就是求两个连通块的最小曼哈顿距离,枚举两个连通块中的点,随后取最小值即可(这题题目的结果要减一)。
abs(x1 - x2) + abs(y1 - y2)
最后我们来分析一下时间复杂度,dfs或bfs
的时间复杂度都是O(n ^ 2)
,求曼哈顿距离的时间复杂度是O(x * (n - x))
(x
表示第一个连通块中的连通块数量),可以发现这是一个基本不等式,最大值为(n ^ 2) / 4
,所以总的时间复杂度就是O(n ^ 2)
。通过本题绰绰有余。
代码
// dfs连通块
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
#define s second
#define f first
using namespace std;
typedef pair<int, int> PII;
const int N = 55;
int n, m;
char map[N][N];
bool read[N][N];
vector<PII> v[3];
int cnt;
int l = 0x3f3f3f3f;
PII s[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
void dfs(int x, int y, vector<PII>& v)
{
read[x][y] = true;
v.push_back({x, y});
for(int i = 0; i < 4; i++)
if(map[x + s[i].f][y + s[i].s] == 'X' && !read[x + s[i].f][y + s[i].s])
dfs(x + s[i].f, y + s[i].s, v);
}
int main()
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%s", map[i] + 1);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(map[i][j] == 'X' && !read[i][j])
dfs(i, j, v[cnt++]);
for(PII a : v[0])
for(PII b : v[1])
{
//printf("111");
l = min(l, abs(a.s - b.s) + abs(a.f - b.f));
}
printf("%d", l - 1);
return 0;
}
大臣的旅费
分析
从题目的描述中可以知道题目给的是一颗树。所以我们每次读取的边的数量就是n - 1
次(这个好像不用我说题目也说了)。而根据题目描述的所花路费只与总路程有关,路费是一个等差数列,所以对于这道题我们只需要求出路中的最长路径随后用前n
项和公式计算即可。
那么怎样求树中的最长路径呢?树的最长路径,其实就是树的直径。
树的直径问题解法一边是使用两次dfs
,一次找到离根节点(对于双向的树来说这个根可以是树中的任何点)最远的点,随后再用一次dfs
遍历出的最长路径就是树的直径。
如何证明呢?
我们先来思考一下从一个点遍历一遍这棵树又回到起点的路径是多长,显而易见,这个路径长是边长和的二倍,在这条路中每条边是都被走了两遍的。
我们若想找到树中的直径显然是不能重复经过一条边的,所以直径要小于边长和的两倍。
我们再思考,直径的理想情况是遍历树中的每一条边,这种情况是有可能的(一条直线),所以树的直径要小于等于树的边长和。
铺垫完了这些我们就可以将求树的直径转化成树的边长和减去不经过的边长的长度,也就是:
d = l - x
d
表示直径,l
表示树边长之和,x
表示未经过的边长,那么问题就转化成了求x
的最小值。
那么如何来求这个最小值呢?为方便理解,主包画了一个草图。
可以明显的看出直径就是最长的那一条(这是废话)。
我们按照我们的算法思路先以A
为根节点找到D
随后再一遍dfs
找到C
,那么从D
到C
就是树的直径。
为何这样是正确的呢?其实不难想,因为我们前面的分析,我们要求直径其实就是求避开的边长的最小值。
而我们在遍历树的过程中每次只记录距离最长的一条边(类似于dp),自然而然的就避开了所有短的边。
又根据树的性质两点之间的路径是唯一的,并且树中不存在环。所以就不存在图中那些弯弯绕,每次丢弃权重最小的一部分即可。
时间复杂度不必多说,两次都是O(n)
代码
/*
树中的最长路,树的直径问题。
先求长度,然后公式求解
*/
#include<iostream>
#include<vector>
#include<cstring>
#define s second
#define f first
using namespace std;
typedef pair<int, int> PII;
typedef long long LL;
const int N = 100010;
int n;
vector<int> tree[N];
vector<int> w[N];
bool read[N];
PII dfs(int v)
{
PII l = {0, v};
read[v] = true;
for(int i = 0; i < tree[v].size(); i++)
{
int z = tree[v][i];
if(!read[z])
{
PII m = dfs(z);
if(m.f + w[v][i] > l.f)
l = {m.f + w[v][i], m.s};
}
}
return l; //最长距离
}
int main()
{
scanf("%d", &n);
for(int i = 1; i < n; i++)
{
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
tree[x].push_back(y);
w[x].push_back(z);
tree[y].push_back(x);
w[y].push_back(z);
}
int i = dfs(1).s;
memset(read, false, sizeof read);
i = dfs(i).f;
//printf("%d", i);
printf("%lld", ((LL)i * (11 + 10 + i))/2);
return 0;
}
飞机降落
分析
第一眼感觉是贪心,但是看到数据量之后发现不是贪心,数据量很小所以我们考虑搜索或状态压缩dp。
初步分析,总共有n
太飞机,每台飞机都要在固定的区间内降落。
发现n
很小,我们可以考虑组合数,而组合数的时间复杂度是n * n!
这道题就是36288000
,三千六百万,再乘上t
就是3.6
亿,时间限制是两秒钟。
虽然组合数的常数很小可能通过但是也有TLE
的风险,考虑优化。
优化不必多说,搜索的常见优化就是打表和剪枝,对于组合数问题打表显然不行,所以我们考虑剪枝。
我们枚举组合数就是枚举每台飞机降落的顺序,显然后面的飞机不可能在前面的飞机降落前完成降落,所以剪枝的判断条件就是后面的飞机能否在前面的飞机完成降落后降落。
还有一步贪心就是如果这台飞机可以降落,我们该选择哪个时间使其降落。很好想尽量让开始降落的时间靠前(和线性dp很像)因为降落时间早的状态是包含降落时间晚的所有状态的。
剪枝后运行时间是38ms,说明我们的代码效率还是很高的。(实际剪枝后的时间复杂度是接近O(n ^ 2)的)
代码
/*
全排列降落顺序即可
*/
#include<iostream>
#include<cstring>
#include<vector>
using namespace std;
const int N = 15;
int t, n;
int l[N], r[N], d[N];
vector<int> vtr;
bool read[N];
bool dfs(int t)
{
if(vtr.size() == n)
return true;
for(int i = 1; i <= n; i++)
{
if(!read[i])
{
if(l[i] + r[i] >= t) //可以放下
{
read[i] = true;
vtr.push_back(i);
if(dfs(max(t, l[i]) + d[i])) return true;
read[i] = false;
vtr.pop_back();
}
}
}
return false;
}
int main()
{
scanf("%d", &t);
while(t--)
{
memset(read, false, sizeof read);
vtr.clear();
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d%d%d", l + i, r + i, d + i);
if(dfs(0))
puts("YES");
else
puts("NO");
}
}
母亲的牛奶
分析
不得不吐槽一下这个名字好怪……
看完题目后发现数据量很小,分析一下能不能直接用搜索。
初步分析的话是可以的,但是我们发现一个问题,那就是不知道搜索什么时候结束。
这个简单,因为每次倒牛奶都不会有损失也不会有增加,所以根据某某定理(主包忘了),在之后某一个时间点的状态一定会和当前状态完全相同。
也就是一个循环,所以我们只需要存储一下每次遍历到的状态,当发现重复遍历时退出即可。
分析一下时间复杂度,我们分析极限情况:
每个桶的大小是小于等于20
的,也就是每个桶有21
种状态。所以总的状态量就是21 * 21 * 21
,大概是一万,通过本题绰绰有余。
代码
#include<iostream>
using namespace std;
const int N = 22;
bool read[N][N][N]; //dfs遇到重复状态时就退出
bool C[N];
int a, b, c;
void dfs(int x, int y, int z)
{
if(read[x][y][z]) return;
read[x][y][z] = true;
if(!x) C[z] = true;
//printf("%d %d %d", x, y, z);
if(x != 0)
{
dfs(max(0, x - (b - y)), min(b, y + x), z);
dfs(max(0, x - (c - z)), y, min(c, z + x));
}
if(y != 0)
{
dfs(min(a, x + y), max(0, y - (a - x)), z);
dfs(x, max(0, y - (c - z)), min(c, z + y));
}
if(z != 0)
{
dfs(min(a, x + z), y, max(0, z - (a - x)));
dfs(x, min(b, y + z), max(0, z - (b - y)));
}
}
int main()
{
scanf("%d%d%d", &a, &b, &c);
dfs(0, 0, c);
for(int i = 0; i < 21; i++)
if(C[i])
printf("%d ", i);
return 0;
}
扫雷
分析
今天的最后一道题(因为主包以前做过千奇百怪的扫雷游戏所以这种题感觉闭着眼睛都能写),不得不感慨,刷那么多题没记住几个,写着玩的东西倒是记忆犹新。
数据量是100 * 300 * 300
,大概是1e7
,所以我们需要将时间复杂度控制在线性。
怎么写呢?我们先根据每个雷的位置处理出每个点应该有的数字,随后我们可以将0
看为连通块,随后我们发现我们在点击非零块的时候一次只能解开一个,而在点击0
时每次可以解开多个块(包括部分非0块)。
所以我们的思路是先解开0
的连通块,随后再统计没有解开的非零块的数量即可。
代码
/*
先找全0的连通块
*/
#include<iostream>
#include<cstring>
#define s second
#define f first
using namespace std;
typedef pair<int, int> PII;
const int N = 310;
int t, n;
char map[N][N];
bool read[N][N];
int st[N][N];
PII s[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {-1, 1}, {1, -1}};
void init()
{
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(map[i][j] == '*')
for(int k = 0; k < 8; k++)
st[i + s[k].f][j + s[k].s]++; //统计数字
}
void dfs(int x, int y)
{
read[x][y] = true;
if(st[x][y] == 0 && map[x][y] == '.')
{
for(int i = 0; i < 8; i++)
if(!read[x + s[i].f][y + s[i].s])
{
read[x + s[i].f][y + s[i].s] = true;
dfs(x + s[i].f, y + s[i].s); //dfs求连通块
}
}
}
int main()
{
scanf("%d", &t);
for(int i = 1; i <= t; i++)
{
memset(read, 0, sizeof read);
memset(st, 0, sizeof st);
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%s", &map[i][1]);
init();
/*for(int i = 1; i <= n;puts(""), i++)
for(int j = 1; j <= n; j++)
printf("%d ", st[i][j]);*/
int l = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(map[i][j] == '.' && !read[i][j] && st[i][j] == 0)
{
//printf("%d %d\n", i, j);
dfs(i, j);
l++;
}
//printf("%d\n", l);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(map[i][j] == '.' && !read[i][j])
l++;
printf("Case #%d: %d\n", i, l);
}
return 0;
}