第七周:
第一题:
题目来源:[P1219 USACO1.5] 八皇后 Checker Challenge - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
一个如下的 6×66×6 的跳棋棋盘,有六个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
上面的布局可以用序列 2 4 6 1 3 5来描述,第 i 个数字表示在第 i 行的相应位置有一个棋子,如下:
行号 1 2 3 4 5 6
列号 2 4 6 1 3 5
这只是棋子放置的一个解。请编一个程序找出所有棋子放置的解。
并把它们以上面的序列方法输出,解按字典顺序排列。
请输出前 3 个解。最后一行是解的总个数。
输入格式
一行一个正整数 n,表示棋盘是 n×n 大小的。
输出格式
前三行为前三个解,每个解的两个数字之间用一个空格隔开。第四行只有一个数字,表示解的总数。
输入输出样例
输入
6
**输出 **
2 4 6 1 3 5
3 6 2 5 1 4
4 1 5 2 6 3
4
说明/提示
【数据范围】
对于 100%的数据,6≤n≤13。
题目翻译来自NOCOW。
USACO Training Section 1.5
解题代码:
#include <iostream>
using namespace std;
int n;
char arr[15][15];
int state[15] = { 0 };
int count_nums = 0, printnums = 3;
bool check(int x, int y) {
for (int i = x, j = y; i >= 0 && j >= 0; i--, j--) {
if (arr[i][j] != '.') {
return false;
}
}
for (int i = x, j = y; i < n && j >= 0; i++, j--) {
if (arr[i][j] != '.') {
return false;
}
}
for (int i = x, j = y; i >= 0 && j < n; i--, j++) {
if (arr[i][j] != '.') {
return false;
}
}
for (int i = x, j = y; i < n && j < n; i++, j++) {
if (arr[i][j] != '.') {
return false;
}
}
return true;
}
void print(int x) {
if (x == n) {
count_nums++;
if (printnums > 0) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (arr[i][j] == 'Q') {
cout << j + 1 << " ";
}
}
} cout << "\n";
printnums--;
}
return;
}
for (int i = 0; i < n; i++) {
if (!state[i] && check(x, i)) {
state[i] = 1;
arr[x][i] = 'Q';
print(x + 1);
state[i] = 0;
arr[x][i] = '.';
}
}
}
int main() {
cin >> n;
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 15; j++) {
arr[i][j] = '.';
}
}
print(0);
cout << count_nums;
return 0;
}
解题思路:
先来看看代码的时间复杂度:
这段代码的时间复杂度主要取决于print
函数,它是一个递归函数,用于尝试在每一行放置皇后。
在每一行,我们有n
个可能的选择(即在该行的n
个位置中选择一个放置皇后),然后我们递归地在下一行做同样的事情。因此,我们有n
个选择,对于每个选择,我们又有n
个选择,依此类推,直到我们到达棋盘的底部。这给出了一个时间复杂度的上界为O(nn),然而,实际的时间复杂度可能会低于这个上界,因为我们在`check`函数中进行了剪枝。如果在某个位置放置皇后会导致冲突,我们就不会在那个位置放置皇后,也就不会探索那个位置以下的所有可能性。这可以显著减少实际的运行时间,尽管在最坏的情况下,时间复杂度仍然是O(nn)。
再来看看代码本身:
用了DFS算法,从第一行开始递归,我们可以知道,n皇后问题,可以认为每一行只用下一个棋,所以状态数组只用一个一维数组来表示当前行是否已经下过,对于每一行,从左到右遍历一次,如果没有下过且对角线检查为true,即下该点,然后递归调用,进入下一层,一直到尽头,打印结果,然后进行回溯。
int n;
:定义棋盘的大小和皇后的数量。char arr[15][15];
:定义一个字符数组来表示棋盘,'.'表示空位,'Q’表示皇后。int state[15] = { 0 };
:定义一个状态数组,用于标记某一行是否已经放置了皇后。int count_nums = 0, printnums = 3;
:count_nums
用于计数所有可能的解决方案,printnums
用于控制打印的解决方案的数量。bool check(int x, int y)
:这是一个检查函数,用于检查在坐标(x, y)处放置皇后是否会导致冲突。void print(int x)
:这是一个递归函数,用于在第x行放置皇后,并递归地在下一行放置皇后。如果已经在所有行中放置了皇后,那么就找到了一个解决方案,count_nums
加1,并打印该解决方案。int main()
:主函数中,首先读取棋盘的大小,然后初始化棋盘,最后调用print(0)
开始放置皇后。在找到所有解决方案后,打印解决方案的数量。
第二题:
题目来源:P1443 马的遍历 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
有一个 n×m 的棋盘,在某个点 (x,y) 上有一个马,要求你计算出马到达棋盘上任意一个点最少要走几步。
输入格式
输入只有一行四个整数,分别为n,m,x,y。
输出格式
一个 n×m 的矩阵,代表马到达某个点最少要走几步(不能到达则输出 −1)。
输入输出样例
**输入 **
3 3 1 1
**输出 **
0 3 2
3 -1 1
2 1 4
说明/提示
数据规模与约定
对于全部的测试点,保证 1≤x≤n≤400,1≤y≤m≤400。
解题代码:
#include <iostream>
#include <queue>
#include <stdio.h>
using namespace std;
bool state[401][401];
int x_m[8] = { 2,1,2,1,-2,-1,-2,-1 };
int y_m[8] = { 1,2,-1,-2,-1,-2,1,2 };
int ans[401][401];
int main() {
int n, m, x, y;
cin >> n >> m >> x >> y;
memset(state, false, sizeof(state));
memset(ans, -1, sizeof(ans));
queue<pair<int, int>> q;
state[x][y] = true;
ans[x][y] = 0;
q.push(make_pair(x,y));
while (!q.empty()) {
int cur_x = q.front().first;
int cur_y = q.front().second;
q.pop();
for (int i = 0; i < 8; i++) {
int x0 = cur_x + x_m[i];
int y0 = cur_y + y_m[i];
if (x0 >= 1 && x0 <= n && y0 >= 1 && y0 <= m && !state[x0][y0]) {
state[x0][y0] = true;
ans[x0][y0] = ans[cur_x][cur_y] + 1;
q.push(make_pair(x0, y0));
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
printf("%-5d", ans[i][j]);
}
printf("\n");
}
return 0;
}
解题思路:
以下是这段代码的详细解题思路:
- 初始化:首先,创建一个二维数组
state
来记录棋盘上的每个格子是否已经被访问过,以及一个二维数组ans
来记录到达每个格子所需的最少步数。注意,先将ans数组初始化为-1,这样,就不用对没有走到的点进行特殊处理。然后,将🐎的初始位置标记为已访问,并将其步数设置为0。 - 广度优先搜索:接下来,使用一个队列来进行广度优先搜索。首先将🐎的初始位置加入队列。然后,当队列不为空时,取出队列的第一个元素,然后尝试将🐎移动到所有可能的位置。如果某个位置没有被访问过,并且在棋盘内,那么就将其标记为已访问,并将其步数设置为当前步数加1,然后将其加入队列。
- 输出结果:最后,遍历
ans
数组,打印出到达每个格子所需的最少步数。
这段代码的关键在于使用广度优先搜索来寻找最短路径。因为广度优先搜索总是优先访问距离起点最近的节点,所以当我们第一次访问到一个节点时,就已经找到了到达该节点的最短路径。这也是为什么我们可以在访问一个节点时立即更新ans
数组的原因。这种方法保证了我们找到的是最短路径,而不是任意路径。
DFS其实也可以处理这个题,不过一般会超时,一般步数问题都要用BFS,dfs是一条路走到死,因此,如果一个点到周围8个可达点位,那么对于每一个点位,对于bfs都是当前点位的次数+1,bfs会遍历当前节点附近的所以可达点,然后弹出当前点,把可达点加入队列,而对dfs而言,他会进入其中一个点,并重复操作,进入第一个点的其中一个点,所以会很耗时,我们还要单独写一个比较最小步数的操作。因此不推荐。
第三题:
题目来源:P1144 最短路计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述
给出一个 N 个顶点 M 条边的无向无权图,顶点编号为 1∼N。问从顶点 1 开始,到其他每个点的最短路有几条。
输入格式
第一行包含 22 个正整数 N,M,为图的顶点数与边数。
接下来 M 行,每行 22 个正整数 x,y,表示有一条由顶点 x 连向顶点 y 的边,请注意可能有自环与重边。
输出格式
共 N 行,每行一个非负整数,第 i 行输出从顶点1到顶点 i 有多少条不同的最短路,由于答案有可能会很大,你只需要输出 mod 100003后的结果即可。如果无法到达顶点 i 则输出 0。
输入输出样例
输入
5 7
1 2
1 3
2 4
3 4
2 3
4 5
4 5
输出
1
1
1
2
4
说明/提示
1到5的最短路有4条,分别为2条 1→2→4→5 和 2条 1→3→4→5(由于4→5 的边有2条)。
对于 20%的数据,1≤N≤100;
对于 60%的数据,1≤N≤10^3;
对于100%的数据,1≤N≤106,1≤*M*≤2×106。
解题代码:
#include <iostream>
#include <queue>
#include <vector>
#include <limits>
#include <string.h>
using namespace std;
#define MAX numeric_limits<int>::max()
#define N 1000010
vector<int>g[N];
bool state[N];
int ans[N] = { 0 };
int dis[N];
int n, m, x, y;
queue<int>q;
void spfa(int x) {
dis[x] = 0;
ans[x] = 1;
state[x] = true;
q.push(x);
while (!q.empty()) {
int cur_h = q.front();
q.pop();
for (int i = 0; i < g[cur_h].size(); i++) {
int cur_t = g[cur_h][i];
if (dis[cur_t] > dis[cur_h] + 1) {
dis[cur_t] = dis[cur_h] + 1;
ans[cur_t] = ans[cur_h] % 100003;
if (!state[cur_t]) {
q.push(cur_t);
state[cur_t] = true;
}
}
else if (dis[cur_t] == dis[cur_h] + 1) {
ans[cur_t] = (ans[cur_t] + ans[cur_h]) % 100003;
}
}
}
}
int main() {
cin >> n >> m;
memset(state, false, sizeof(state));
for (int i = 0; i < N; i++) {
dis[i] = MAX;
}
while (m--) {
cin >> x >> y;
g[x].push_back(y);
g[y].push_back(x);
}
spfa(1);
for (int i = 1; i <= n; i++) {
cout << ans[i] << endl;
}
return 0;
}
解题思路:
首先,定义了一些全局变量和数组:
N
是节点的最大数量。g
是一个邻接表,用来存储图的信息。state
数组用来标记每个节点是否加入过队列。ans
数组用来存储每个节点的最短路径数量。dis
数组用来存储从源节点到每个节点的最短距离。q
是一个队列,用来存储待处理的节点。
然后,定义了一个spfa
函数,这个函数实现了SPFA算法。这个函数首先将源节点的距离设为0,路径数量设为1,并将其加入队列。然后,当队列不为空时,取出队列的头部元素,遍历其所有邻接节点,如果找到了一条更短的路径,就更新这个节点的距离和路径数量,并将其加入队列。如果这个节点的距离没有变化,但是找到了一条新的路径,就更新这个节点的路径数量。
最后,在main
函数中,首先读入节点数和边数,然后读入每条边的信息,并构建邻接表。然后,调用spfa
函数计算最短路径和路径数量。最后,输出每个节点的路径数量。
下面是代码更通俗易懂的解释:
首先将dis定义为一个极限值,易于更新,然后使用邻接表存储边与边直接的关系(注意要正反存储,因为是无向图),然后从起始点开始遍历,将他加入队列,然后依次遍历它的所有临边,dis用来存储最小路径,最开始dis[1]==0,然后起点的临边与dis[1]+1比较(即与上一层比较),因为下一层比上一层的最短路距离大一,如果新找到一条更短路径,就更新dis,注意,最开始是一定会更新的,因为dis初始化为一个极限值,如果cur_t
没有加入过队列中,就将其加入队列,并将state[cur_t]
设为true
。这是因为,如果cur_t
已经加入过队列中,那么就没有必要再次将其加入队列,这一步是防止忽略了某个只存在于某个节点的路径中的节点。然后更新ans为其上一个节点的答案,因为它们共用路径,如果,新的节点路径与最短路径相同,则ans就等于两个节点的路径之和。
第四题:
题目来源:23. 矩阵中的路径 - AcWing题库
题目描述:
请设计一个函数,用来判断在一个矩阵中是否存在一条路径包含的字符按访问顺序连在一起恰好为给定字符串。
路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。
如果一条路径经过了矩阵中的某一个格子,则之后不能再次进入这个格子。
注意:
- 输入的路径字符串不为空;
- 所有出现的字符均为大写英文字母;
数据范围
矩阵中元素的总个数 [0,900][0,900]。
路径字符串的总长度 [1,900][1,900]。
样例
matrix=
[
["A","B","C","E"],
["S","F","C","S"],
["A","D","E","E"]
]
str="BCCE" , return "true"
str="ASAE" , return "false"
解题代码:
class Solution {
public:
int state[901][901]={0};
int idx=0;
int x_m[4]={1,0,0,-1};
int y_m[4]={0,-1,1,0};
bool search(int x,int y,int len,int rows,int cols,vector<vector<char>>& matrix,string &str){
idx++;
if(idx==len){
return true;
}
for(int i=0;i<4;i++){
int x0=x+x_m[i];
int y0=y+y_m[i];
if(x0>=0&&x0<rows&&y0>=0&&y0<cols&&!state[x0][y0]&&matrix[x0][y0]==str[idx]){
state[x0][y0]=1;
if(search(x0,y0,len,rows,cols,matrix,str)) return true;
state[x0][y0]=0;
}
}
return false;
}
bool hasPath(vector<vector<char>>& matrix, string &str) {
int len = str.size();
int rows = matrix.size();
if(rows==0){
return false;
}
int cols = matrix[0].size();
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(str[0]==matrix[i][j]){
state[i][j]=1;
if(search(i,j,len,rows,cols,matrix,str)){
return true;
}
state[i][j]=0;
}
idx=0;
}
}
return false;
}
};
解题思路:
以下是这段代码的详细解题思路:
- 初始化:首先,创建一个二维数组
state
来记录矩阵中的每个格子是否已经被访问过,以及一个变量idx
来记录当前已经匹配的字符数量。然后,定义四个方向的移动向量x_m
和y_m
,用于在矩阵中上下左右移动。 - 深度优先搜索:
search
函数是一个递归函数,用于进行深度优先搜索。它首先检查当前已经匹配的字符数量是否等于目标字符串的长度,如果是,那么就返回true
表示找到了一条路径。然后,尝试将当前位置移动到四个方向的每一个位置。如果新的位置在矩阵内,没有被访问过,并且字符与目标字符串的下一个字符匹配,那么就将其标记为已访问,并递归调用search
函数。如果递归调用返回true
,那么就返回true
。否则,就将新的位置标记为未访问。 - 查找路径:
hasPath
函数用于查找一条路径。它首先获取目标字符串的长度,以及矩阵的行数和列数。然后,遍历矩阵中的每一个格子,如果字符与目标字符串的第一个字符匹配,那么就将其标记为已访问,并调用search
函数。如果search
函数返回true
,那么就返回true
。否则,就将当前格子标记为未访问,并继续查找。
这段代码的关键在于使用深度优先搜索来查找路径。因为深度优先搜索可以深入到某一条路径的尽头,所以它可以找到所有可能的路径。然后,通过回溯(即将已访问的格子标记为未访问),我们可以在找到一条路径后继续查找其他路径。这就是这段代码的解题思路。
本题与第二题不同,此题不需要解决步数问题,而是查找路径,正好需要深度优先,一直走到尽头。
第五题:
题目来源:207. 课程表 - 力扣(LeetCode)
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
例如,先修课程对 [0, 1]
表示:想要学习课程 0
,你需要先完成课程 1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
示例 1:
输入:numCourses = 2, prerequisites = [[1,0]]
输出:true
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
输出:false
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
提示:
1 <= numCourses <= 2000
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i]
中的所有课程对 互不相同
解题代码:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int>indegree(numCourses,0);
vector<vector<int>>matrix(numCourses, vector<int>(numCourses));
for(int i=0;i<prerequisites.size();i++){
indegree[prerequisites[i][0]]++;
matrix[prerequisites[i][1]][prerequisites[i][0]]++;
}
queue<int> que;
for (int i = 0; i < numCourses; ++i) {
if (indegree[i] == 0) que.push(i);
}
int count=0;
while(!que.empty()){
int cur=que.front();
que.pop();
count++;
for(int i=0;i<matrix.size();i++){
if(matrix[cur][i]>0){
matrix[cur][i]--;
indegree[i]--;
if(!indegree[i]){
que.push(i);
}
}
}
}
return count==numCourses;
}
};
解题思路:
这段代码是解决“课程表”问题的一个解决方案,它使用了拓扑排序算法。这个问题的目标是确定是否可以完成所有课程的学习。在这个问题中,课程被表示为图的顶点,而先修课程的要求则被表示为有向边。
以下是这段代码的详细解题思路:
- 初始化:首先,创建一个一维数组
indegree
来记录每个课程的入度(即需要先修的课程数量),以及一个二维数组matrix
来记录课程之间的依赖关系。然后,遍历先修课程的列表,更新indegree
和matrix
。 - 创建队列:接下来,创建一个队列,将所有入度为0的课程(即没有先修课程要求的课程)加入队列。
- 处理队列:然后,当队列不为空时,取出队列的第一个课程,将已完成的课程数量加1,然后遍历该课程的所有后续课程。如果后续课程的入度大于0,那么就将其入度减1,并检查新的入度是否为0。如果是,那么就将其加入队列。
- 检查结果:最后,检查已完成的课程数量是否等于总课程数量。如果是,那么就返回
true
表示可以完成所有课程的学习。否则,就返回false
。
这段代码的关键在于使用拓扑排序来处理课程的依赖关系。因为拓扑排序可以保证每个课程在其所有的先修课程之后被处理,所以我们可以通过这种方法来检查是否可以完成所有课程的学习。