76. 最大效益
题目
问题描述
明明的爸爸开了一家小公司,公司里有5名职员。今天,公司接待了5位客户。明明的爸爸知道,和任何一位客户谈判并签下合同都要花一整天的时间,而他又希望在一天之内,和这5位客户都签好合同。因此,明明的爸爸要求公司里的5名职员分别与1位客户谈判。
明明的爸爸也知道,这5名职员和5位客户的性格各不相同。因此,不同的职员与不同的客户谈判,会给公司带来不同的经济效益。他现在要做出一个决策,让5名职员分别与哪位客户谈判,才能让公司今天的总经济效益最大。
明明的爸爸首先做了一张5行5列的效益表,如下所示:
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
在这张效益表中,每行代表一名公司职员,每列代表一个客户,每行中的5个数字就表示了当该行所代表的公司职员和每位客户谈判时所能产生的效益。明明的爸爸就要通过这张效益表来决定哪位职员与哪位顾客谈判,然后能够使公司的效益最大。就拿上面这张表来看,由于无论哪位职员与哪位客户谈判,所产生的效益都是1,因此最大的效益就是5。这是最简单的一种情况,但是当效益表里的数字变得复杂,就很难进行选择,到底哪种组合方式才是最优的。因此明明的爸爸求助于你,帮助他解决这个问题。
明明的爸爸的问题可以归结为:给你一张5行5列的效益表,表中的数字均为大于等于0的整数,要求在这张表中选出5个数字,使这5个数字的和最大。(注:这5个数字分别来自表中的不同行不同列,即同一行只能选择一个数字,同一列也只能选择一个数字。)
个人总结
思路
本题需要从5行5列的矩阵中选出不同行不同列的5个数字,使其和最大。
由于数据量极小,采用全排列暴力枚举列号组合是最稳妥的算法。
设立一个初始元素为0到4的列号数组。由于数组下标天生代表行号,数组内的值代表列号,以此天然保证了不同行不同列。
利用next_permutation函数配合do-while循环,穷举此列号数组的所有排列情况,即遍历了所有可能的签合同匹配方式。 对每种排列计算一次总效益,通过比对不断更新保留最大值即为最终答案。
易错点
- 因为多组输入的触发条件通常设定为尝试读取矩阵的第一个位置,所以在后续嵌套循环读取整个矩阵剩余部分时,必须设置条件跳过行标和列标均为0的情况,否则会吃掉后续数据导致整个矩阵错位。
- 调用next_permutation获取全部排列时,要求目标数组或容器最初必须是升序状态,否则仅仅会输出字典序处于其后的排列组合,造成情况遗漏。
- 必须使用do-while结构执行排列枚举,如果使用while结构,程序会在进入第一次循环体前就执行了状态切换,立刻覆盖掉初始升序情况,从而永远漏算第一种匹配可能。
知识点 - next_permutation
1. next_permutation 是什么?
next_permutation 是 C++ STL(标准模板库)中的一个函数,包含在 <algorithm> 头文件里。 它的作用是:求出一个序列的全排列中,按字典序排在当前序列后面的下一个排列。
比如序列 {1, 2, 3},按字典序的全排列共有 6 种:
- 1 2 3
- 1 3 2
- 2 1 3
- 2 3 1
- 3 1 2
- 3 2 1
如果当前数组是 {1, 2, 3},调用一次这个函数,数组就会变成 {1, 3, 2},并且函数返回 true。 如果当前数组已经是最后一个排列 {3, 2, 1},调用这个函数,数组会被重新变回最小的 {1, 2, 3},并且函数返回 false。
2. 怎么用?
在 OJ 中,通常结合数组或者 vector 使用。
最关键的易错点 :由于它只是求"下一个"排列,如果你想拿到所有 的全排列,你的初始数组必须是升序的 。如果是乱序的,你必须先用 sort 给它排个序。
用法示例:
cpp
#include <iostream>
#include <vector>
#include <algorithm> // 必须包含这个头文件
using namespace std;
int main() {
int a[3] = {1, 2, 3};
// 注意:如果初始是 {2, 1, 3},则只会输出 213 以后的排列,所以有必要的话先 sort(a, a+3);
do {
// 输出当前排列
for(int i = 0; i < 3; i++) {
cout << a[i] << " ";
}
cout << "\n";
} while (next_permutation(a, a + 3)); // 范围是 左闭右开 [首地址, 尾地址)
return 0;
}
如果是 vector<int> v,则写成 next_permutation(v.begin(), v.end())。
3. 为什么要用 do while 而不是直接用 while?
这是很多初学者容易踩坑的地方。我们来看看两者的区别:
如果用 while:
cpp
int a[3] = {1, 2, 3};
while (next_permutation(a, a + 3)) {
// 循环体第一遍执行时,a 里面已经是 {1, 3, 2} 了!
}
程序在判断 while 条件时,会先执行一次 next_permutation。这会导致你的初始状态 {1, 2, 3} 直接被覆盖成下一个状态 {1, 3, 2},从而漏掉了最初始的这一种情况。
如果用 do while:
cpp
int a[3] = {1, 2, 3};
do {
// 循环体第一遍执行时,a 还是 {1, 2, 3}
} while (next_permutation(a, a + 3));
do while 的特点是先执行一次循环体,再去判断条件 。 这样程序会先把 {1, 2, 3} 这个初始情况处理完(比如算一次效益、输出一次结果),然后再调用 next_permutation 变成 {1, 3, 2} 供下一次循环使用。这样就能完美保证所有的排列一种都不漏地被处理。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int a[5][5];
while (cin >> a[0][0]) {
// 输入矩阵
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
if (i == 0 && j == 0) {
// 第一个数字已经输入过了,跳过
continue;
}
cin >> a[i][j];
}
}
// 计算每种情况
int cols[] = {0,1,2,3,4};
int s = 0;
do {
int sum = 0;
for (int i = 0; i < 5; i++) {
sum += a[i][cols[i]];
}
s = max(s,sum);
} while (next_permutation(cols,cols+5));
cout << s << "\n";
}
return 0;
}
77. 螺旋方阵
题目
问题描述
明明在上学的时候,参加数学兴趣班。在班上,老师介绍了一种非常有趣的方阵,称之为螺旋方阵。该方阵一共由n×n个正整数构成(我们称之为n阶螺旋方阵),即共有n行n列。
方阵中的数字从1开始递增,数字的排序规则是从左上角出发由1开始排序,并按顺时针方向旋进,即先排最外面的一圈,然后排里面的一圈,以此类推,直到排到最后一个数为止。
例如一个4阶的螺旋方阵,一共有4×4=16个正整数构成,数字从1递增到16,最后排出来的方阵如下:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7
明明回家后想自己动手构造这样的螺旋方阵。他从n=1开始构造,但是他发现当n越来越大时,螺旋方阵的复杂性就越高,然后构造出来的方阵就越容易出错。为了降低构造方阵的出错率,提高构造速度,明明就求助于你,请你帮他写一个程序,来构造螺旋方阵。 明明的问题可以归结为:给你一个正整数n,请你按题目描述中所述的方法,构造出n阶的螺旋方阵。
输入说明
你写的程序要求从标准输入设备中读入测试数据作为你所写程序的输入数据。标准输入设备中有多组测试数据,每组测试数据仅占一行,每行仅有一个正整数n(1≤n≤10),即所要构造的螺旋方阵的阶数。每组测试数据与其后一组测试数据之间没有任何空行,第一组测试数据前面以及最后一组测试数据后面也都没有任何空行。
输出说明
对于每一组测试数据,你写的程序要求计算出一组相应的运算结果,并将这一组运算结果作为你所写程序的输出数据依次写入到标准输出设备中。每组运算结果为一个n阶的螺旋方阵,方阵中的数字用一个空格隔开,具体形式请参考输出样例。每组运算结果与其后一组运算结果之间有一个空行,最后一组运算结果之后没有空行。 注:通常,显示屏为标准输出设备。
个人总结
思路
使用二维数组模拟方阵,初始化所有元素为0表示未填充。
设定当前坐标(x, y)和初始方向变量(如0代表右,1代表下,2代表左,3代表上)。
循环填入数字1到n*n。每次填入后,根据当前方向预判下一步坐标。
检查下一步是否越界(x,y < 0 或 >= n)或该位置已被填充(数值不为0)。
如果下一步受阻,顺时针改变方向并更新坐标;如果未受阻,直接沿当前方向更新坐标。
易错点
- 变量初始化:全局变量每次循环要先初始化,否则会出现第一组数据正常,后续出错的情况
- 判断顺序:在检查下一步是否被填充前,必须先判断坐标是否在数组下标合法范围内,防止数组越界访问。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
// 方向,0123 分别代表向右向下向左和向上(按照转圈顺序)
int d = 0;
// 坐标
int x = 0, y = 0;
int a[15][15];
void solv(int n) {
switch (d) {
case 0:
if (y+1 >= n || a[x][y+1] != 0) {
d = 1; // 转向
x++;
} else {
y++;
}
break;
case 1:
if (x+1 >= n || a[x+1][y] != 0) {
d = 2; // 转向
y--;
} else {
x++;
}
break;
case 2:
if (y-1 < 0 || a[x][y-1] != 0) {
d = 3; // 转向
x--;
} else {
y--;
}
break;
case 3:
if (x-1 < 0 || a[x-1][y] != 0) {
d = 0; // 转向
y++;
} else {
x--;
}
break;
}
return;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int n;
bool f = true;
while (cin >> n) {
// 初始化
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 15; j++) {
a[i][j] = 0;
}
}
d = 0;
x = 0, y = 0;
for (int i = 1; i <= n*n; i++) {
a[x][y] = i;
// 更新坐标
solv(n);
}
if (f) {
f = false;
} else {
cout << "\n";
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (j != 0) {
cout << " ";
}
cout << a[i][j];
}
cout << "\n";
}
}
return 0;
}
78. 方块转换
题目
问题描述
一块N x N(1=<N<=10)正方形的黑白瓦片的图案要被转换成新的正方形图案。
写一个程序来找出将原始图案按照以下列转换方法转换成新图案的最小方式:
#1:转90度:图案按顺时针转90度。
#2:转180度:图案按顺时针转180度。
#3:转270度:图案按顺时针转270度。
#4:反射:图案在水平方向翻转(形成原图案的镜像)。
#5:组合:图案在水平方向翻转,然后按照#1-#3之一转换。
#6:不改变:原图案不改变。
#7:无效转换:无法用以上方法得到新图案。
如果有多种可用的转换方法,请选择序号最小的那个。
比如:
转换前:
@-@
@@-
转换后:
@-@
@--
--@
这种转换采取#1(按顺时针转90度)即可。
注意:图案中的字符"@"和"-"在转90度后,还是"@"和"-"。不要认为"-"转90度后变成"|"。
输入说明
第一行: 单独的一个整数N。
第二行到第N+1行: N行,每行N个字符(不是'@'就是'-');这是转换前的正方形。
第N+2行到第2*N+1行: N行,每行N个字符(不是'@'就是'-');这是转换后的正方形。
输出说明
单独的一行包括1到7之间的一个数字(在上文已描述)表明需要将转换前的正方形变为转换后的正方形的转换方法。
个人总结
思路
- 利用二维vector容器存储正方形图案。借助标准库容器重载的比较运算符直接对比两个矩阵是否一致,省去多重循环手写比较过程。
- 将最核心的顺时针转九十度与水平反射独立封装为底层函数。
- 建立多重旋转校验辅助函数,内部循环调用转九十度函数并对比,一次性覆盖操作选项中的前三种旋转情况。
- 判断逻辑严格按照题目列出的一到七号操作次序从上到下执行,遇到符合条件的转换立即输出并结束程序,从而确保在存在多种转换路径时必然输出最小的序号。
易错点
- 顺时针旋转九十度的坐标映射关系极易写错。原矩阵位置对应的行列索引应转换为新矩阵位置的列索引与反向行索引。
- 水平反射是按中央垂直中轴线进行左右镜像翻转,只变换列数索引,容易与沿主对角线的转置混合。
- 原地覆盖问题。对矩阵进行空间坐标变换时读和取如果针对同一块内存,极易导致旧数据丢失产生污染错误。必须先用与原尺寸一致的局部临时矩阵暂存变换过程,全图遍历完成后再统一赋值修改原矩阵。
- 容易忽视题目要求的优先级。如果把不改变的特判或反射的校验前置,在满足多个转换条件下会得出错误的转换序号,必须严格按一到七顺次书写判断分支。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
void rotate(vector<vector<char>> &b, vector<vector<char>> &a,int N) {
// a 矩阵旋转九十度后存入 b 矩阵
vector<vector<char>> tmp(N, vector<char>(N,0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
tmp[j][N - i - 1] = a[i][j];
// 调试
/*
for (int k = 0; k < N; k++) {
for (int h = 0; h < N; h++) {
cout << tmp[k][h] << " ";
}
cout << "\n";
}
cout << "\n";
*/
}
}
b = tmp;
return;
}
void reflect(vector<vector<char>> &b, vector<vector<char>> &a,int N) {
// a 矩阵水平翻转后存入 b 矩阵
vector<vector<char>> tmp(N,vector<char>(N,0));
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
tmp[i][j] = a[i][N - j - 1];
}
}
b = tmp;
return;
}
int mul_rotate(vector<vector<char>> &a, vector<vector<char>> &t, int N) {
// 多次旋转 a 矩阵看是否与 t 矩阵相同,如果相同则返回旋转次数,如果连续三次还找不到则返回 0
vector<vector<char>> tmp(N,vector<char>(N,0));
tmp = a;
for (int i = 1; i <= 3; i++) {
rotate(tmp,tmp,N);
if (t == tmp) {
return i;
}
}
return 0; // 没找到
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
int N;
cin >> N;
vector<vector<char>> o(N, vector<char>(N,0)); //原始矩阵
vector<vector<char>> t(N, vector<char>(N,0)); // 目标矩阵
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
cin >> o[i][j];
}
}
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
cin >> t[i][j];
}
}
vector<vector<char>> b(N, vector<char>(N,0));
// 1:转90度:图案按顺时针转90度。
// 2:转180度:图案按顺时针转180度。
// 3:转270度:图案按顺时针转270度。
int cnt = mul_rotate(o,t,N);
if (cnt != 0) {
cout << cnt;
return 0;
}
// 4:反射:图案在水平方向翻转(形成原图案的镜像)。
reflect(b,o,N);
if (b == t) {
cout << 4;
return 0;
}
// 5:组合:图案在水平方向翻转,然后按照#1-#3之一转换。
// 把翻转后的矩阵放入 b
reflect(b,o,N);
// 旋转 b 并对比
cnt = mul_rotate(b,t,N);
if (cnt != 0) {
cout << 5;
return 0;
}
// 6:不改变:原图案不改变。
if (o == t) {
cout << 6;
return 0;
}
// 7:无效转换:无法用以上方法得到新图案。
cout << 7;
return 0;
}
计算机英语翻译练习
To return, then, to theswitching capabilities of a modern computer:computers in the 1970s were generally able to handle eight switches at a time. That is, they could deal with eight binary digits , or bits, of data, at every cycle. A group of eight bits is called a byte , each byte containing 256 possible patterns of ONs and OFFs (or 1s and Os). Each pattern is the equivalent of an instruction, a part of an instruction, or a particular type of datum , such as a number or a character or a graphics symbol. The pattern 11010010, for example, might be binary data---in this case, the decimal number 210---or it might be an instruction telling the computer to compare data stored in its switches to data stored in a certain memory-chip location.
那么,回到现代计算机的开关能力:20世纪70年代的计算机,通常一次能处理八个开关。也就是说,它们可以在每个周期内处理八个二进制数字或比特的数据。
8 个比特为一组,称为一个字节,每个字节包含 256 种可能的"开"与"关"(或 1 与 0)组合模式。每种模式都等同于一条指令、指令的一部分或某种特定类型的数据,例如数字、字符或图形符号。
例如,11010010 这种模式可能是二进制数据------在本例中即十进制数 210------也可能是一条指令,告诉计算机将其开关中存储的数据与存储在某个内存芯片位置的数据进行比较。
The development of processors that can handle 16, 32, and 64 bits of data at a time has increased the speed of computers. The complete collection ofrecognizable patterns---the total list of operations---of which a computer is capable is called its instruction set. Both factors-the number of bits that can be handled at one time, and the size of instruction set scontinue to increase with the ongoing development of modern digital computers.
能够一次处理 16 位、32 位和 64 位数据的处理器的发展,提高了计算机的速度。计算机所能识别的完整模式集合------即其全部操作列表------被称为其指令集。
随着现代数字计算机的不断发展,一次可处理的位数和指令集的规模这两个因素都在持续增长。
ll. Hardware
Modern digital computers are all conceptually similar, regardless of size. Nevertheless, they can be divided into several categories on the basis of cost and performance: the personal computer or microcomputer , a relatively low-cost machine, usually of desktop size (though "laptops" are small enough to fit in a briefcase , and "palmtops " can fit into a pocket); the workstation , a microcomputer with enhanced graphics and communications capabilities that make it especially useful for office work; the minicomputer , generally too expensive for personal use, with capabilities suited to a business, school, or laboratory; and the mainframe computer, a large, expensive machine with the capability of serving the needs of major business enterprises, government departments, scientific research establishments, or the like (the largest and fastest of these are called supercomputers).
II. 硬件
现代数字计算机无论尺寸大小,在概念上都是相似的。
尽管根据成本和性能,它们可以分为几个类别:个人计算机或微型计算机,这是一种成本相对较低的机器,通常为台式大小(尽管"笔记本电脑"小到可以装入公文包,而"掌上电脑"可以装入口袋);工作站,一种具有增强图形和通信能力的微型计算机,使其特别适用于办公;小型机,通常对于个人使用来说过于昂贵,其性能适用于企业、学校或实验室;以及大型机,这是一种大型且昂贵的机器,能够满足大型企业、政府部门、科研机构等的需求(其中规模最大、速度最快的被称为超级计算机)。
背单词打卡截图
