BUAA 计组 26 年上机题(更新中)
P0,P1 当时觉得太宝宝巴士了,所以没记。P2 感觉上了一点强度,而且 P3 考前想找往年题最后只找到了 \(6\) 道,所以觉得还是记录一下让后人乘凉吧。
这里推销一下我和一个学长共同开发的 VS Code 插件:BUAA CO Toolkit,波形图比 ISE 好看很多哦。
P2

P2 的话注意利用好宏,我进考场第一件事就是敲了五六个宏。
T1
给定一个 0 和 1 构成的 \(n\times m\) 的网络,1 代表陆地, 0 代表水域,求所有陆地的总周长。
周长定义为:
对于一个陆地格,它的每条边按以下规则计算:
-
如果边的另一侧是水域,该边对总周长贡献。
-
如果边的另一侧超出网格范围,该边对总周长贡献。
-
如果边的另一侧也是陆地,该边不计入总周长。
网格可以包含多个岛屿,输出所有陆地的总周长即可。陆地内部被包围的水域与陆地之间的边界也计入总周长。
这题没有参考的 C 语言代码。
第一题实际上就比较有强度了,我考场上写了很多 if,注意判断边界然后标签太多注意命名格式,我是采用 loop_<第一层标号>_<第二层标号>... 这种类似的命名格式的。然后注意循环之后要自增和跳回!一般来说循环有两类写法:
Loop:
...
beq $t0, $t1, Exit
j Loop
Exit:
和
Loop:
...
bne $t0, $t1, Loop
后者当然不会忘记跳回,而且实际上能少一步,前者可能会忘记。但值得一提的是这两者的逻辑是不一样的,前者类似 while 后者类似 do while,还是要做区分。
最后的代码:
Code
.data
Grid: .space 2000
.macro done
li $v0, 10
syscall
.end_macro
.macro input(%reg)
li $v0, 5
syscall
move %reg, $v0
.end_macro
.macro get_pos(%pos, %x, %y)
move %pos, %x
mult %pos, $s1
mflo %pos
add %pos, %pos, %y
.end_macro
.text
input($s0)
input($s1)
li $t0, 0
loop_1:
li $t1, 0
loop_1_1:
get_pos($t2, $t0, $t1)
sll $t2, $t2, 2
input($t3)
sw $t3, Grid($t2)
addi $t1, $t1, 1
blt $t1, $s1, loop_1_1
addi $t0, $t0, 1
blt $t0, $s0, loop_1
li $s2, 0
li $t0, 0
loop_2:
li $t1, 0
loop_2_1:
get_pos($t2, $t0, $t1)
sll $t2, $t2, 2
lw $t2, Grid($t2)
beq $t2, $0, if_end
addi $t5, $t0, -1
blt $t5, 0, if_1
get_pos($t3, $t5, $t1)
sll $t3, $t3, 2
lw $t3, Grid($t3)
bne $t3, $0, if_1_end
addi $s2, $s2, 1
j if_1_end
if_1:
addi $s2, $s2, 1
if_1_end:
addi $t5, $t0, 1
bge $t5, $s0, if_2
get_pos($t3, $t5, $t1)
sll $t3, $t3, 2
lw $t3, Grid($t3)
bne $t3, $0, if_2_end
addi $s2, $s2, 1
j if_2_end
if_2:
addi $s2, $s2, 1
if_2_end:
addi $t6, $t1, -1
blt $t6, 0, if_3
get_pos($t3, $t0, $t6)
sll $t3, $t3, 2
lw $t3, Grid($t3)
bne $t3, $0, if_3_end
addi $s2, $s2, 1
j if_3_end
if_3:
addi $s2, $s2, 1
if_3_end:
addi $t6, $t1, 1
bge $t6, $s1, if_4
get_pos($t3, $t0, $t6)
sll $t3, $t3, 2
lw $t3, Grid($t3)
bne $t3, $0, if_4_end
addi $s2, $s2, 1
j if_4_end
if_4:
addi $s2, $s2, 1
if_4_end:
if_end:
addi $t1, $t1, 1
bne $t1, $s1, loop_2_1
addi $t0, $t0, 1
bne $t0, $s0, loop_2
li $v0, 1
move $a0, $s2
syscall
done
T2
根据前序遍历序列和中序遍历序列写出后序遍历,这题哪怕放 DS 期末估计都要斩杀一片人吧,给出了 C 语言代码,不过第二题就考递归吗,还是太难了。
参考代码
#include <stdio.h>
#define MAXN 205
int n;
int pre[MAXN];
int in[MAXN];
void build(int pl, int pr, int il, int ir) {
if (pl > pr) {
return;
}
int root = pre[pl];
// Locate the root in the inorder sequence
int k = il;
while (in[k] != root) {
k++;
}
int leftSize = k - il;
// Left subtree
build(pl + 1, pl + leftSize, il, k - 1);
// Right subtree
build(pl + leftSize + 1, pr, k + 1, ir);
printf("%d\n", root);
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &pre[i]);
}
for (int i = 0; i < n; i++) {
scanf("%d", &in[i]);
}
build(0, n - 1, 0, n - 1);
return 0;
}
实现要点:注意循环变量也得入栈,注意函数参数也得入栈,注意在函数参数调用前就给他入栈,好了上代码:
Code
.data
pre: .space 820
in: .space 820
space: .asciiz " "
enter: .asciiz "\n"
.macro space
la $a0, space
li $v0, 4
syscall
.end_macro
.macro endl
la $a0, enter
li $v0, 4
syscall
.end_macro
.macro input(%reg)
li $v0, 5
syscall
move %reg, $v0
.end_macro
.macro done
li $v0, 10
syscall
.end_macro
.macro push(%arg)
add $sp, $sp, -4
sw %arg, 0($sp)
.end_macro
.macro pop(%arg)
lw %arg, 0($sp)
add $sp, $sp, 4
.end_macro
.text
input($s0)
li $t0, 0
for1:
input($t1)
sll $t0, $t0, 2
sw $t1, pre($t0)
srl $t0, $t0, 2
addi $t0, $t0, 1
bne $t0, $s0, for1
li $t0, 0
for2:
input($t1)
sll $t0, $t0, 2
sw $t1, in($t0)
srl $t0, $t0, 2
addi $t0, $t0, 1
bne $t0, $s0, for2
push($ra)
push($a0)
push($a1)
push($a2)
push($a3)
li $a0, 0
subi $a1, $s0, 1
li $a2, 0
subi $a3, $s0, 1
jal build
nop
pop($a3)
pop($a2)
pop($a1)
pop($a0)
pop($ra)
done
build:
push($t0)
push($t1)
push($t2)
bgt $a0, $a1, return
sll $t0, $a0, 2
lw $t0, pre($t0)
move $t1, $a2
loop:
sll $t1, $t1, 2
lw $t2, in($t1)
srl $t1, $t1, 2
beq $t2, $t0, loop_end
addi $t1, $t1, 1
j loop
loop_end:
nop
sub $t2, $t1, $a2
push($ra)
push($a0)
push($a1)
push($a2)
push($a3)
add $a1, $a0, $t2
addi $a0, $a0, 1
subi $a3, $t1, 1
jal build
nop
pop($a3)
pop($a2)
pop($a1)
pop($a0)
pop($ra)
push($ra)
push($a0)
push($a1)
push($a2)
push($a3)
addi $a0, $a0, 1
add $a0, $a0, $t2
addi $a2, $t1, 1
jal build
nop
pop($a3)
pop($a2)
pop($a1)
pop($a0)
pop($ra)
push($a0)
li $v0, 1
move $a0, $t0
syscall
pop($a0)
push($a0)
endl
pop($a0)
return:
pop($t2)
pop($t1)
pop($t0)
jr $ra
注意 build 函数内的循环必须是 while,我考场上把 do while 改 while 的时候忘记 j loop 了,硬控一小时。
写这篇 blog 的时候一样被卡了一小时
T3
简要题意:计算将数组的各个分组分别变成回文序列所需的最少删除次数。
分组方式是给定一个 \(k\),然后定义等价类为 \([r] = \{r+kt|1\le r+kt \le n, k\in \mathbb{Z},1\le r\le n\}\)。
这题也给出了参考代码,但是参考代码的写法调用了两层函数,这还是太恶心了,而且区间 DP 的写法非常蛋疼,需要先枚举 len;于是我的写法把第一个函数写进 main 了。
参考代码
#include <stdio.h>
#define MAX_N 30
int minPalindromeDeletes(const int *subseq, int m) {
if (m <= 1) return 0;
int dp[MAX_N][MAX_N];
for (int i = 0; i < m; i++) dp[i][i] = 0;
for (int len = 2; len <= m; len++) {
for (int l = 0; l + len <= m; l++) {
int r = l + len - 1;
if (subseq[l] == subseq[r]) {
dp[l][r] = (len == 2) ? 0 : dp[l + 1][r - 1];
} else {
int a = dp[l + 1][r];
int b = dp[l][r - 1];
dp[l][r] = (a < b ? a : b) + 1;
}
}
}
return dp[0][m - 1];
}
int kPalindrome(const int *arr, int n, int k) {
int ans = 0;
int subseq[MAX_N];
for (int r = 0; r < k; r++) {
int m = 0;
for (int j = r; j < n; j += k) {
subseq[m++] = arr[j];
}
ans += minPalindromeDeletes(subseq, m);
}
return ans;
}
int main(void) {
int n, k, arr[MAX_N];
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d", &arr[i]);
scanf("%d", &k);
printf("%d\n", kPalindrome(arr, n, k));
return 0;
}
注意 Flatten 的时候要把横轴最长长度传进去,你可以直接传 MAX_N 也可以传每次都在变的 m。推荐选前者,因为后者会让你突然想起来你传的其实是 n。不过这题其实没啥太大问题,因为 \(m < n\),就是内存会不连续。
然后注意在调用第二个函数的时候不要偷懒,寄存器有点不太够用,你得把主函数的循环变量也压栈。
最后注意两个 if 块内的三目运算符,这样其实有四个 if 块,既然你都是肉编器了那还是优化一下写法,给这四个 if 块设计一个统一出口,这也符合 C 语言中 goto 语句的使用规范
Code
.data
arr: .space 240
subsq: .space 240
dp: .space 4000
.macro input(%reg)
li $v0, 5
syscall
move %reg, $v0
.end_macro
.macro push(%arg)
add $sp, $sp, -4
sw %arg, 0($sp)
.end_macro
.macro pop(%arg)
lw %arg, 0($sp)
add $sp, $sp, 4
.end_macro
.macro done
li $v0, 10
syscall
.end_macro
.macro get_pos(%pos, %x, %y, %m)
move %pos, %x
mult %pos, %m
mflo %pos
add %pos, %pos, %y
.end_macro
.text
input($s0)
li $t0, 0
loop1:
sll $t0, $t0, 2
input($t1)
sw $t1, arr($t0)
srl $t0, $t0, 2
addi $t0, $t0, 1
bne $t0, $s0, loop1
input($s1)
#kPalindrome
li $s2, 0
li $t0, 0
loop2:
li $t1, 0
move $t2, $t0
loop21:
sll $t2, $t2, 2
lw $t3, arr($t2)
sw $t3, subsq($t1)
addi $t1, $t1, 4
srl $t2, $t2, 2
add $t2, $t2, $s1
blt $t2, $s0, loop21
push($ra)
push($a0)
push($t0)
push($t1)
srl $t1, $t1, 2
move $a0, $t1
jal minPalindromeDeletes
pop($t1)
pop($t0)
pop($a0)
pop($ra)
add $s2, $s2, $v0
addi $t0, $t0, 1
bne $t0, $s1, loop2
move $a0, $s2
li $v0, 1
syscall
done
minPalindromeDeletes:
li $v0, 0
ble $a0, 1, return
li $t0, 0
for_1:
get_pos($t1, $t0, $t0, $a0)
sll $t1, $t1, 2
sw $0, dp($t1)
addi $t0, $t0, 1
bne $t0, $a0, for_1
li $t0, 2
for2:
li $t1, 0
for21:
add $t2, $t1, $t0
subi $t2, $t2, 1
sll $t1, $t1, 2
sll $t2, $t2, 2
lw $t4, subsq($t1)
lw $t5, subsq($t2)
srl $t1, $t1, 2
srl $t2, $t2, 2
get_pos($t6, $t1, $t2, $a0)
bne $t4, $t5, else1
li $t4, 2
beq $t0, $t4, else12
addi $t1, $t1, 1
subi $t2, $t2, 1
get_pos($t4, $t1, $t2, $a0)
addi $t1, $t1, -1
subi $t2, $t2, -1
sll $t4, $t4, 2
sll $t6, $t6, 2
lw $t5, dp($t4)
sw $t5, dp($t6)
srl $t4, $t4, 2
srl $t6, $t6, 2
j if_end
else12:
sll $t6, $t6, 2
sw $0, dp($t6)
srl $t6, $t6, 2
j if_end
else1:
addi $t1, $t1, 1
get_pos($t4, $t1, $t2, $a0)
addi $t1, $t1, -1
subi $t2, $t2, 1
get_pos($t5, $t1, $t2, $a0)
subi $t2, $t2, -1
sll $t4, $t4, 2
sll $t5, $t5, 2
sll $t6, $t6, 2
lw $t4, dp($t4)
lw $t5, dp($t5)
blt $t4, $t5, assigna
addi $t5, $t5, 1
sw $t5, dp($t6)
j if_end
assigna:
addi $t4, $t4, 1
sw $t4, dp($t6)
if_end:
addi $t1, $t1, 1
add $t3, $t1, $t0
ble $t3, $a0, for21
addi $t0, $t0, 1
ble $t0, $a0, for2
sub $t0, $a0, 1
get_pos($t1, $0, $t0, $a0)
sll $t1, $t1, 2
lw $v0, dp($t1)
return:
jr $ra
如果这题 C 语言写法换成记忆化搜索那将绝杀
P3
P3 的难度突然骤降了,搞得我都不想更新了。
一进机房我发现所有电脑上都开着虚拟机,我还以为魔改版的 MIPS 只能在 Linux 环境下跑,于是我看了下环境文档,结果啥也没有。
P.S. 据同学反映此次课下的 IM 指令范围规格好像和考试不太一样,所以尽可能开到 ROM 的最大位数吧。
此外写测试脚本的时候注意你的 CPU 没有实现 addiu,所以不要写 li,赋值时用 ori。
T1
题目大意是把 GPR[rs] 和 GPR[rt] 看作两个 01 串,求它们的 \(\operatorname{Hamming}\) 距离并存入 GPR[rd]。
以防有同学不知道 \(\operatorname{Hamming}\) 距离,我还是写一下 RTL:
temp <- GPR[rs] xor GPR[rt]
GPR[rd] <- temp_0 + temp_1 + ... + temp_{31}
注意 Logisim 中 BitAdder 的使用,这是一个可以求这个数字内 \(1\) 的个数的元件。R 型指令还是挺简单的。
T2
名字是 lcbeq(Load Compare Branch if Equal),大概就是魔改 beq,描述如下:
ext_offset <- (sign_extend(offset) << 2)_{31...0}
addr <- GPR[rs] + ext_offset
temp <- Mem[addr]_{31...0}
if temp == GPR[rt]
PC <- PC + 4 + ext_offset
else
PC <- PC + 4
乍一看似乎需要改 ALU 的输入端,但其实不能这样,会破坏我们原本的数据流。我的做法是在 DM 下游增加一个 lcbeq_en 模块,执行判断逻辑,然后将这个模块的输出和 Zero 或一下。注意修改一下 NPC,ext_offset 的拓展方式和原本的方式有细微区别。剩下的工作就是改 ALU 里 addr 的计算。
T3
名字是 lwbr,这题算是个奇奇怪怪的 R 型指令,要改的东西蛮多的,描述如下:
addr <- GPR[rs] + GPR[rt]
temp <- Mem[addr]
if temp == 0
PC <- GPR[rd]
else
GPR[rd] <- tmp
初看这条指令觉得很简单,仔细一想有两个卡点:
- 如果你让
GRF的两个读入口接rs和rt,那temp == 0时PC的数据来源GPR[rd]就读不到。因此应该增加一个GRF读入端口。 NPC似乎要新加入GPR[rd]的读入端口,这样似乎要增大NPC_Sel的位宽(当然如果你是奇奇怪怪的写法当我没说),会非常麻烦,所以我的办法是在GRF(原本是jr,jalr指令时读GPR[rs]的端口)上游接一个 MUX,然后选择信号由DM下游的lwbr_check统一输出。
这时候奇异写法的优势就体现出来了,同学们可以学学 winrs 学长的绿框写法。
对于第一个卡点由于我的 GRF 封装了一个 reg,reg 用来存放我的寄存器池,因此我加指令只需要添加一个 reg,但有的同学就没这么方便了。
然后 T3 是唯一一道我没有一遍过的,因为 T3 我这边涉及到六个信号的修改,而我没有统一给 lwbr_check 的使能信号 lwbr_en 赋初值。这时候就要讲一种赋初值的好办法了——高阻电阻阻塞:


浙公网安备 33010602011771号