- 第 198 篇 -
Date: 2026 - 03- 31 | 周二
Author: 郑龙浩(仟墨)
今日算法:DFS & 记忆化搜索 & 回溯
2026-03-31-算法打卡day35-DFS专项训练2
文章目录
- 2026-03-31-算法打卡day35-DFS专项训练2
7-洛谷P15435-免费披萨-蓝桥杯2025国赛Python大学B组
DFS
【题目】
蓝桥小镇披萨店的老板刚刚烤制了他人生中的第 n 个披萨!为了庆祝这一重要时刻,他推出了一项名为"幸运订单"的活动,顾客有机会赢取免费披萨。以下是活动的具体规则:
- 生成订单编号:每位顾客需要生成一个九位数的订单编号。生成方法如下:首先,将数字 1 到 8 进行任意排列(每个数字正好出现一次),组成一个八位数。然后,在这个八位数的任意位置(可以是开头、结尾或中间)插入一个 1 到 8 的数字,从而得到一个九位数的订单编号。
- 计算最大公约数,赢取免费披萨:披萨店老板会计算每位顾客生成的订单编号与 n 的最大公约数(GCD)。如果某个订单编号与 n 的最大公约数最大,那么该顾客就有机会赢得免费披萨。注意:订单编号必须严格满足上述生成规则,如果有多个订单编号与 n 的最大公约数相同且达到最大值,则只有生成数值最小的订单编号的顾客能够获奖。
现在,小蓝也想参加这个活动,并希望赢取免费披萨。请你帮助小蓝找出能够让他赢得免费披萨的订单编号。
输入格式
输入一行包含一个八位的正整数 n,表示披萨店老板烤制的第 n 个披萨。
输出格式
输出一行包含一个九位的正整数表示答案,即小蓝能够赢得免费披萨的最小订单编号。
输入输出样例
12345678
输出
415637826
说明/提示
评测用例规模与约定
对于所有评测用例,107≤n<108。
【思路】
整理题目含义:
输入一个n,找到一个数字n2。n2的条件:与n有着相同因数(保证这个「因数」最大) && 且数字在1e8到1e9-1之间(数字由1-8组成,且7个数字只出现一次,只有1个数字是2次)
思路:
1 输入n,从n自身开始,从大到小遍历所有的因数,假设因数为yinshu
2 利用这个因数,反向找到对应想要的数字
- 先找出第一个>=1e8且能被yinshu整除的数字,记为start。start就是可能满足条件的第一个8位数
- 实现方法:从1e8开始到1e9-1循环,直到找到第一个能被yinshu整除的数字
- 如果找不到start,就表示当前因数yinshu,说明当前
yinshu没有对应的 9 位数倍数
- 从start开始,每次增加yinshu,得到可能的结果curNum
- 如果curNum在
1e8~1e9-1范围内,就检查是否符合合法订单编号(全都是1-8组成,且每个数字出现了1次,只有一个数字出现了2次)
- 如果curNum在
- 如果出现了这个数字,直接输出即可(满足数字范围 && 满足订单格式)
下面代码只有90分,有2个案例超时了,其他的还好
【代码】
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 7-洛谷P15435-免费披萨-暴力枚举-蓝桥杯2025国赛Python大学B组
* Author:郑龙浩
* Date:2026-03-31
* 算法:
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 检查num是否是合法订单编号
bool IS(ll num) {
int numCnt[9] = {0};
// string = to_string(num);
while (num) {
if (num % 10 == 9 || num % 10 == 0) return false;
numCnt[num % 10]++; // 求出数字数量
num /= 10;
}
int oneCnt = 0; // 数字数量为1
int twoCnt = 0; // 为2
for (int i = 1; i <= 8; i++) {
if (numCnt[i] == 1) oneCnt++;
else if (numCnt[i] == 2) twoCnt++;
else {
return false; // 如果有1个不是数量为1或2,直接return false
}
}
if (oneCnt == 7 && twoCnt == 1) return true;
else return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
ll N; cin >> N;
ll n = N;
ll yinshu;
// 枚举所有可能的因子
for (int i = n; i >= 1; i--) {
if (n % i == 0) { // 找到了因数
yinshu = i;
// 找到[1e8, 1e9-1]之间的可以被yinshu整除的数字(yinshu对应的倍数)
ll start;
for (start = 1e8; start < 1e9; start++) {
if (start % yinshu == 0) // 找到对应的第一个yinshu的倍数了
break;
}
// 步长是yinshu,这样可以保证curNum都是因数
for (ll curNum = start; curNum < 1e9; curNum += yinshu) { // 这样就可以保证都是yinshu的倍数了,也就是所有的curNum和n处理起来,都是相同的最大公因数,都是yinshu
if (IS(curNum) == true) { // 如果该数既是yinshu的倍数 && 也是合法订单编号
cout << curNum;
return 0;
}
}
}
}
return 0;
}
8-洛谷P6183-The_Rock_Game_S-DFS
这个我不会,先跳过了
【题目】
题目描述
在奶牛回家休息和娱乐之前,Farmer John 希望它们通过玩游戏获得一些智力上的刺激。
游戏板由 n n n 个相同的洞组成,这些洞最初都是空的 。一头母牛要么用石头盖住一个空的洞,要么揭开一个先前被盖住的洞。游戏状态的定义是所有洞是否被石头覆盖的情况。
游戏的目标是让奶牛到达每个可能的游戏状态一次,最后回到初始状态。
以下是他们其中一次游戏的示例(空的洞用 O 表示,用石头盖住的洞用 X 表示):
| 时刻 | 洞 1 | 洞 2 | 洞 3 | 描述 |
|---|---|---|---|---|
| 0 0 0 | O | O | O | 一开始所有的洞都是空的 |
| 1 1 1 | O | O | X | 盖上洞 3 |
| 2 2 2 | X | O | X | 盖上洞 1 |
| 3 3 3 | X | O | O | 打开洞 3 |
| 4 4 4 | X | X | O | 盖上洞 2 |
| 5 5 5 | O | X | O | 打开洞 1 |
| 6 6 6 | O | X | X | 盖上洞 3 |
| 7 7 7 | X | X | X | 盖上洞 1 |
现在牛被卡住玩不下去了!他们必须打开一个洞,然而不管他们打开哪个洞,他们都会到达一个他们已经到达过的状态。例如,如果他们从第二个洞中取出岩石,他们将到达他们在时刻 2 2 2 已经访问过的状态(X O X)。
下面是一个 3 个孔的有效解决方案:
| 时间 | 洞 1 | 洞 2 | 洞 3 | 描述 |
|---|---|---|---|---|
| 0 0 0 | O | O | O | 一开始所有的洞都是空的 |
| 1 1 1 | O | X | O | 盖上洞 2 |
| 2 2 2 | O | X | X | 盖上洞 3 |
| 3 3 3 | O | O | X | 打开洞 2 |
| 4 4 4 | X | O | X | 盖上洞 1 |
| 5 5 5 | X | X | X | 盖上洞 2 |
| 6 6 6 | X | X | O | 打开洞 3 |
| 7 7 7 | X | O | O | 打开洞 2 |
| 8 8 8 | O | O | O | 打开洞 1,恢复到原来的状态 |
现在,奶牛们厌倦了这个游戏,它们想找你帮忙。
给定 n n n,求游戏的有效解决方案序列。如果有多个解决方案,则输出任意一个。
输入格式
仅一行,一个整数 n n n。
输出格式
共 2 n + 1 2^n+1 2n+1 行,每行一个长度为 n n n 的字符串,其中只包含字符 O 和 X,该行中的第 i i i 个字符表示第 i i i 个孔在此状态下是被覆盖还是未覆盖,第一行和最后一行必须全部都是 O。
输入输出样例 #1
输入 #1
3
输出 #1
OOO
OXO
OXX
OOX
XOX
XXX
XXO
XOO
OOO
说明/提示
对于 100 % 100\% 100% 的数据,有 1 ≤ n ≤ 15 1\le n\le15 1≤n≤15。
【思路】
【代码】
cpp
9-洛谷P13886-分糖果-DFS-蓝桥杯2023省PythonA
DFS搜索
难点在判断条件上以及两个for
【题目】
题目描述
两种糖果分别有 9 9 9 个和 16 16 16 个,要全部分给 7 7 7 个小朋友,每个小朋友得到的糖果总数最少为 2 2 2 个最多为 5 5 5 个,问有多少种不同的分法。糖果必须全部分完。
只要有其中一个小朋友在两种方案中分到的糖果不完全相同,这两种方案就算作不同的方案。
输入格式
无
输出格式
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只输出这个整数,填写多余的内容将无法得分。
【思路】
整理题目:
有7个小朋友,也就意味着要给7个位置分配糖果
那么递归的深度就是7
dfs return 条件:
如果到了第7层 && a == 0 && b == 0,也就是糖果也分完了,直接总方案数ans++且return
如果到了第7层,糖果没有分完的话,方案数不++,因为当前的分配方案是不合适,直接return去尝试其他的结果好了
递归函数DFS中
递归当中可以写两个for 循环,第一层for 分配糖果a(可以分配05个),第二层分配糖果b(可以分配05个)
因为糖果每次最少分2个,最多分5个
此时就要判断,a糖果+ b糖果是否在 2到5这个区间内,只有在这个区间内的,才可以dfs递归,去尝试下一次的分配
【代码】
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 9-洛谷P13886-分糖果-DFS-蓝桥杯2023省PythonA
* Author:郑龙浩
* Date:2026-03-31
* 算法:DFS
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a = 9, b = 16;
ll ans;
// pos: 当前分配了pos个位置 或 已经分配到了第pos个位置(pos位置已经分配了)
void dfs(int pos) {
if (pos == 7) { // 当已经分配了7个位置时
if (a == 0 && b == 0) ans++; // 如果糖果全部用过,就说明有了一个方案,直接ans ++
return; // 如果发糖果没有用光,也要return,返回回溯去找下一个结果
}
for (int i = 0; i <= 5; i++) {
if (a - i < 0) break;
a -= i; // a糖果分配了i个
for (int j = 0; j <= 5; j++) {
if (b - j < 0) break;
int total = i + j; // 当前分配糖果的数量
if (total < 2) continue; // 如果给该同学分配的糖果数量 < 2,则表示不符合分配条件,那么就不能分,直接continue,去寻找下一个符合的分配方案去
if (total > 5) break; // 如果给该同学分配的糖果数量大于5,那么也就没有继续分的必要了,后面的所有情况肯定都不符合
// 不能分配的情况提前处理, 如果能分配就会走到这一步,直接分配就好了
b -= j; // b糖果分配了j个
// 分配了一次,就+1一次
dfs(pos + 1);
b += j; // 回溯
}
a += i; // 回溯
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
dfs(0); // 刚开始一个也没分配呢,也就是分配了0次
cout << ans;
return 0;
}
10-洛谷P2404-自然数的拆分问题
DFS & 回溯
【题目】
题目描述
任何一个大于 1 1 1 的自然数 n n n,总可以拆分成若干个小于 n n n 的自然数之和。现在给你一个自然数 n n n,要求你求出 n n n 的拆分成一些数字的和。每个拆分后的序列中的数字从小到大排序。然后你需要输出这些序列,其中字典序小的序列需要优先输出。
输入格式
输入:待拆分的自然数 n n n。
输出格式
输出:若干数的加法式子。
输入输出样例 #1
输入 #1
7
输出 #1
1+1+1+1+1+1+1
1+1+1+1+1+2
1+1+1+1+3
1+1+1+2+2
1+1+1+4
1+1+2+3
1+1+5
1+2+2+2
1+2+4
1+3+3
1+6
2+2+3
2+5
3+4
说明/提示
数据保证, 2 ≤ n ≤ 8 2\leq n\le 8 2≤n≤8。
【思路】
模板题,套模板罢了
注意:
dfs的开始数字是1,且下一个数字的选择范围正确,以及确定找到一个方案的时候的数量是2即可
【代码】
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 10-洛谷P2404-自然数的拆分问题
* Author:郑龙浩
* Date:2026-03-31
* 算法:DFS
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int N;
vector <vector <int>> results;
vector <int> path;
int sum = 0; // 和
// curNum表示当前需要处理的数字 (当前可以选择的最小数字(保证拆分序列非降序,避免重复))
void dfs(int curNum) {
if (sum == N && path.size() >= 2) { // 终止条件:当前和等于N 且 至少有两个数(题目要求拆分成至少两个自然数)
results.push_back(path); // 保存当前方案
return;
}
// 剩余需要凑的和
int remaining = N - sum; // 还有remaining剩余数字没有加入sum,所以后面可以尝试从curNum 到 remaining加入到sum中去
// 遍历所有可能的下一个数字
// 从curNum到remaining:保证数字不降序,且不超过剩余和
for (int nextNum = curNum; nextNum <= remaining; nextNum++) { // 可以有多个相同的数字相加,所以nextNum下一次加入的数字也可以是上一次加入的数字curNum
path.push_back(nextNum); // 选择当前数字
sum += nextNum; // 更新当前和
dfs(nextNum); // 递归处理,传入nextNum保证非降序
path.pop_back(); // 回溯,撤掉选择,尝试下一个选择(撤掉刚才的节点(下面的),然后网上走一步,再向下一个节点前进)
sum -= nextNum;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> N;
dfs(1);
for (auto i : results) {
int len = i.size();
for (int j = 0; j < len - 1; j++) cout << i[j] << '+';
cout << i[len - 1] << '\n'; // 最后一个是换行,不是+
}
return 0;
}
11-洛谷P2386-放苹果-DFS回溯
DFS & 回溯
【题目】
题目描述
把 m m m 个同样的苹果放在 n n n 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法。( 5 , 1 , 1 5,1,1 5,1,1 和 1 , 1 , 5 1,1,5 1,1,5 是同一种方法)
输入格式
第一行是测试数据的数目 t t t,以下每行均包括二个整数 m m m 和 n n n,以空格分开。
输出格式
对输入的每组数据 m m m 和 n n n,用一行输出相应的结果。
输入输出样例 #1
输入 #1
1
7 3
输出 #1
8
输入输出样例 #2
输入 #2
3
3 2
4 3
2 7
输出 #2
2
4
2
说明/提示
对于所有数据,保证: 1 ≤ m , n ≤ 10 1\leq m,n\leq 10 1≤m,n≤10, 0 ≤ t ≤ 20 0 \leq t \leq 20 0≤t≤20。
【思路】
模板题上稍加修改
【代码】
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 11-洛谷P2386-放苹果-DFS回溯
* Author:郑龙浩
* Date:2026-03-31
* 算法:DFS & 回溯
* n个盘子,每个盘子可放入0~m个苹果
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int cnt = 0;
int n, m;
// 注意求的是 组合数,不是排列数,所以为了保证所有的方案都是不重复的,
// 我求方案数的时候保证其每个盘子苹果数量,是递增的即可,也就是不可能出现123,又出现了321这种情况的分配,保证递增的顺序就保证了盘子分配苹果的方案是求的组合数,而不是排列数的方案了
// pos:分配到了第pos个盘子
// lastNum // 上一个盘子放的苹果数量 // 刚开始我没有写第二个参数,导致了我的方案数出现了好多排列,比如111出现了三次,或者123,321,132,312等都出现了,但实际上这个是一种情况
// 写了这个参数后,111只会出现一次,或者122也只会出现一次,或者123也只会出现一次...
void dfs(int pos, int lastNum) {
// pos == n + 1为终止条件
if (pos > n) { // 如果处理到了第n + 1个盘子,就表示前面n个盘子都装过了,也就是有了一套方案了
if (m == 0) cnt++; // 如果苹果用完了,择表示当前这个方案是可以的,直接计入总方案数即可
return; // 如果m个苹果没有用完,择表示虽然装盘成功,有了一套方案,但是苹果并没有使用完,也不可以,返回去尝试寻找下一个方案就好了
}
// 剪枝是AI帮我加的,我自己不太能想到
// 如果剩余盘子,都放如最小的苹果数量,还超过m个苹果(剩余的苹果的话)
// 也就是,怎么放,m个苹果都不够用的,既然每个盘子放最小的苹果都不够,那么大的也是不够的,那么执行后面的代码只会浪费空间和时间,所以去掉
int remaining = n - pos + 1; // 剩余的盘子
if (m < remaining * lastNum) return; // 如果剩余的苹果不够分的,也就不需要分了
// 剪枝
for (int i = lastNum; i <= m; i++) { // 每次可以放入上一个盘子放的苹果数量 ~ m个苹果(m是剩余的苹果数量)
// 如果放了i个苹果后,苹果是负的,择不符合常理,盘子里也不能放这些了,既然i个不能放入盘子中了,那么所有大于i的苹果的数量都不能放入盘子中了,就无需再判断了,直接break,其实相当于return了,因为break后必会执行return(隐性)
m -= i;
dfs(pos + 1, i);
m += i;
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> m >> n;
cnt = 0; // 每次计算分法之前,都要重置为0,否则方案数量cnt会继承上一组数据继续计算,会出现叠加的错误
dfs(1, 0);
cout << cnt << '\n';
}
return 0;
}
12-洛谷P6566-观星-DFS搜图
DFS 搜图
【题目】
题目描述
Jimmy 和 Symbol 约好一起看星星,浩瀚的星空可视为一个长为 N N N、宽为 M M M 的矩阵,矩阵中共有 N × M N\times M N×M 个位置,一个位置可以用坐标 ( i , j ) (i,j) (i,j)( 1 ≤ i ≤ N 1\le i\le N 1≤i≤N, 1 ≤ j ≤ M 1\le j\le M 1≤j≤M)来表示。每个位置上可能是空的,也可能有一个星星。
对于一个位置 ( i , j ) (i,j) (i,j),与其相邻的位置有左边、左上、上面、右上、右边、右下、下面、左下 8 个位置。相邻位置上的星星被视为同一个星座,这种关系有传递性,例如若 ( 1 , 1 ) , ( 1 , 2 ) , ( 1 , 3 ) (1,1),(1,2),(1,3) (1,1),(1,2),(1,3) 三个
位置上都有星星,那么这三个星星视为同一个星座。包含的星星数量相同的星座被视为一个星系(一个星系中的星座不一定相邻),星系的大小为星系中包含的所有星星数量。
由于 Symbol 太喜欢星系了,他就想考一考 Jimmy,让 Jimmy 求出星空中有多少个星系,他还想知道,最大的星系有多大。
输入格式
第一行两个整数 N , M N,M N,M 表示矩阵的长宽。
接下来 N N N 行每行 M M M 个字符,每个字符只可能是.或*。这 N N N 行中第 i i i 行的第 j j j 个字符是*表示位置 ( i , j ) (i,j) (i,j) 上有一个星星,否则表示它是空的。
输出格式
仅一行两个整数,用空格分隔开,分别表示星系的数量与最大星系的大小。
输入输出样例 #1
输入 #1
5 7
*......
..**..*
.*...*.
...*...
....*..
输出 #1
3 4
输入输出样例 #2
输入 #2
10 10
**..**.**.
***....*..
*...**.**.
...*..*...
..........
**...**.*.
..*.*....*
..........
***..*.*..
.***..*...
输出 #2
4 12
说明/提示
对于 20 % 20\% 20% 的数据, N , M ≤ 20 N,M\le 20 N,M≤20,最大星系大小不超过 200。
对于 50 % 50\% 50% 的数据, N , M ≤ 400 N,M\le 400 N,M≤400。
对于 70 % 70\% 70% 的数据, N , M ≤ 1100 N,M\le 1100 N,M≤1100。
对于 100 % 100\% 100% 的数据, 2 ≤ N , M ≤ 1500 2\le N,M\le 1500 2≤N,M≤1500,最大星系大小不超过 100000。
【思路】
这是一个很明显的「搜图DFS」
分析题目:
星座:相连的星星(8个方向)
星系:星星数量相同的所有星座,不必相邻
求啥:求星系的数量 & 最大的星系是多大(星系的数量是所有星星之和)
我的思路:
dfs查找星座,给星座命名,且计算出该星座多大,将数量存入数组中
vector <int> Cnts; 从0开始命名星座,索引为名,数值为数量
map表是:key为星星明,val为星星数
遍历完所有的星座后
给Cnts数组排序,从小到大排序,为了方便计算相同数量星座的数量
创建 totalCnt,存储星系数量,初始化为1
创建Max,存储最大星系的和,初始化为0
创建curXingxiCnt = Cnts[0];存储星系的星星数量,初始化为第一个星座星星数量
如果只有1个星座,直接输出就好了,后面不用执行,记得break
遍历Cnts数组,从索引1开始遍历,如果当前的星座和前一个星座数量相同,就将当前星座数量加入curXingxiCnt
如果当前星座与前一个星座不相同的话,也就意味着当前位置是下一个星座,
- 此时就用Max与上一个星系对比,保留最大的星系
- 然后curXingxiCnt 重置为 当前星座数量(也就是不保留前面上一个星系的数量,重新开始计数)
- totalCnt计数
最后输出星系数量totalCnt & 最大Max星系数量
【代码1 】存储星座星星数量,然后计算星系数量和最大星系的星星数
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 12-洛谷P6566-观星-DFS搜图
* Author:郑龙浩
* Date:2026-03-31
* 算法:DFS遍历图
* 方法1:存储星座,然后计算多少个星系
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int N, M;
vector <int> Cnts;
int cnt;
int direction[8][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, -1}, {-1, 1}, {1, 1}, {-1, -1}};
void dfs(vector <vector<char>>& graph, vector <vector<bool>>& visited, int x, int y) {
visited[x][y] = true;
cnt++;
for (int i = 0; i < 8; i++) {
int nextX = x + direction[i][0];
int nextY = y + direction[i][1];
if (nextX < 0 || nextX >= N || nextY < 0 || nextY >= M) continue;
if (graph[nextX][nextY] == '*' && visited[nextX][nextY] == false)
dfs(graph, visited, nextX, nextY);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> N >> M;
vector <vector <char>> graph(N, vector <char>(M));
vector <vector <bool>> visited(N, vector <bool>(M));
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) cin >> graph[i][j];
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) {
if (graph[i][j] == '*' && visited[i][j] == false) {
cnt = 0;
dfs(graph, visited, i, j);
Cnts.push_back(cnt);
}
}
int len = Cnts.size();
if (len == 0) {
cout << 0 << ' ' << 0;
return 0;
}
if (len == 1) {
cout << 1 << ' ' << Cnts[0];
return 0;
}
sort(Cnts.begin(), Cnts.end());
int totalCnt = 1; // 星系数量
int Max = Cnts[0]; // 最大星系的星星的数量
int curXingxiCnt = Cnts[0]; // 星系中的星星数量
for (int i = 1; i < len; i++) {
if (Cnts[i] == Cnts[i - 1]) curXingxiCnt += Cnts[i];
else if (Cnts[i] != Cnts[i - 1]) {
Max = max(Max, curXingxiCnt);
curXingxiCnt = Cnts[i];
totalCnt++;
}
}
// 处理最后一组
Max = max(Max, curXingxiCnt);
cout << totalCnt << ' ' << Max;
return 0;
}
【代码2】直接存储每个星系的星星数量,以及星系的每个星座的星星数量
cpp
/* 2026-03-31-算法打卡day35-DFS专项训练2
* 12-洛谷P6566-观星-DFS搜图
* Author:郑龙浩
* Date:2026-04-01
* 算法:DFS
* 方法2:存储星系,而不是存储星座
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int N, M;
vector <int> Cnts;
int cnt;
int direction[8][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}, {1, -1}, {-1, 1}, {1, 1}, {-1, -1}};
unordered_map <int, int> xingzuoXingxi; // 星座大小 -> 星系总星星数
void dfs(vector <vector<char>>& graph, vector <vector<bool>>& visited, int x, int y) {
visited[x][y] = true;
cnt++;
for (int i = 0; i < 8; i++) {
int nextX = x + direction[i][0];
int nextY = y + direction[i][1];
if (nextX < 0 || nextX >= N || nextY < 0 || nextY >= M) continue;
if (graph[nextX][nextY] == '*' && visited[nextX][nextY] == false)
dfs(graph, visited, nextX, nextY);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> N >> M;
vector <vector <char>> graph(N, vector <char>(M));
vector <vector <bool>> visited(N, vector <bool>(M));
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) cin >> graph[i][j];
for (int i = 0; i < N; i++) for (int j = 0; j < M; j++) {
if (graph[i][j] == '*' && visited[i][j] == false) {
cnt = 0;
dfs(graph, visited, i, j);
if (xingzuoXingxi.find(cnt) == xingzuoXingxi.end()) xingzuoXingxi[cnt] = cnt;
else xingzuoXingxi[cnt] += cnt;
}
}
int len = xingzuoXingxi.size();
if (len == 0) {cout << "0 0"; return 0;}
int Max = 0;
for (auto it : xingzuoXingxi) {
Max = max(Max, it.second);
}
cout << len << ' ' << Max;
return 0;
}