Loading

P2-汇编语言

通过阅读本文,您的收获可能有:理解递归程序的本质,知道如何用汇编语言去写dfs,知道P2考试重点要考察的内容。更优质的内容可以移步roife.github.io,roife yyds!

省流助手:过P2需要熟练掌握递归程序的汇编实现、数组(含二维数组)的操作、把C程序翻译成汇编,另外要会用断点调试 or 有优秀的静态调试能力,最好事先熟悉一下已经学过的数据结构和算法。

听说我押中了正考两题和后面的补考两题hhh

upd:19级新考了素数判断,约瑟夫环,快排,可见程设课难度的题都有可能放P2考。另外似乎无脑压栈会导致TLE了,所以压栈的时候要思考一下,哪些是必须压栈的,哪些是可以变的,以及是否能提前算好栈大小,而不是用一个格子就减掉一个格子的位置。

upd: 20级同学的话可能会面对一个造数据的神,上面提到的问题可能真的要仔细思考一下。B站搜索咕咕香,可以找到该神的汇编程序设计补充教程,他会在视频中演示如何用汇编打一棵平衡树等操作。

课下测试部分:

今天晚上才刚开始写作业,目前只写了前两个,感觉和P1课上的时候一样,代码写得慢。基本的对二维数组的操作和多重循环中的例行公事部分(修改计数器,条件判断)有时候会忘记。

矩阵乘法:

基本的二维数组操作,在做之前我特意比较了一下P2教程中的二维数组操作和预习教程中的二维数组操作,发现P2写的类似于使用do while型的循环,有一定的优势,比如do while省标签个数,代码量也相对于for循环少。出现的问题有:循环实现慢(老毛病),写错了一个换行向量的判断条件(比较对象写错了),还有多重循环有一处没有刷新计数变量。此处我用的do while型的循环写的,果然节省了标签和码量,以后的题可以再试试。

回文串:

一维数组操作,比较简单,但题目提示中关于syscall中8和12的区别还是需要注意的:12是单字符读入,类似于getchar()(?),而8是一行读入,有点像fgets(),会把换行读进去,且需要预先设定到底最多读入多少个字符。几个判断函数需要好好熟悉一下,看看啥时候该用什么。一般for循环喜欢用beq,do while喜欢用bne

卷积运算:

弱化版的卷积运算,不需要填充,也不需要翻转,没有花里胡哨的操作,只要老老实实操作二维数组就行了。在下手之前一定要把地址计算和下标范围给解决了,只有数学上写的是对的,程序才有可能对。下面给出C++代码,注释部分是对于地址计算和下标的思考。

 1 #include <bits/stdc++.h> 
 2 int ans[10][10];
 3 int m1,n1,m2,n2;
 4 int core[10][10],matrix[10][10];
 5 void Convolution();
 6 void print();
 7 int main() {
 8     scanf("%d %d %d %d",&m1,&n1,&m2,&n2);
 9     for(int i=0;i<m1;i++){
10         for(int j=0;j<n1;j++){
11             scanf("%d",&matrix[i][j]);
12         }
13     }
14     for(int i=0;i<m2;i++){
15         for(int j=0;j<n2;j++){
16             scanf("%d",&core[i][j]);
17         }
18     }
19     Convolution();
20     print();
21     return 0;
22 }
23 //本题不需要翻转,也不需要填充 
24 //下面用变量i,j控制卷积核的移动,k,l作为数据运算时的卷积核中元素行列标号
25 //分析可知:0<=i<=m1-m2   0<=j<=n1-n2    
26 //分析可知:0<=k<m2       0<=l<n2    
27 //计算规则:ans[i][j]+=core[k][l]*matrix[i+k][j+l]
28 void Convolution(){
29     for(int i=0;i<=m1-m2;i++){
30         for(int j=0;j<=n1-n2;j++){
31             for(int k=0;k<m2;k++){
32                 for(int l=0;l<n2;l++){
33                     ans[i][j]+=core[k][l]*matrix[i+k][j+l];
34                 }
35             }
36         }
37     }
38 }
39 void print(){
40     for(int i=0;i<=m1-m2;i++){
41         for(int j=0;j<=n1-n2;j++){
42             printf("%d ",ans[i][j]);
43         }
44         printf("\n");
45     }
46 }

 

本题我处理不好的地方:虽然一遍就过样例了,但是编写过程中还是出了一些问题:首先是滥用a0寄存器。这不是我的本意,我真的是用完了t和s,然后想暂时借用一下a0,结果写到最后发现需要输出字符的时候要覆盖掉a0的值,这样我就白忙活了。情急之下,我每次打印字符之前都先把a0拷贝一份,打印完字符之后再拷贝回来,做到了代码改动最少,但是看起来却特别的傻。如果没有意识到这一点,不知道会错成什么样子。第二个问题是a与s打混了,就是因为滥用a0,所以敲着敲着发现有个应该是s0的地方被写成了a0,as在键盘相邻,被打错的可能行还是有的。最后一个问题是mflo打错了,打成了mulo,结果还真有一个mulo,功能大概是考虑溢出的乘法,这个错误也是写着写着扫了一眼发现的,太低级的失误了。

全排列:

典型递归程序,也是P2考试的重点。如果不清楚递归程序写法的,请先在OJ上重新完成全排列问题以及汉诺塔问题。如果不太懂得如何用汇编实现递归程序,请仔细研读下面的汉诺塔汇编代码。以及如果做了哈密顿回路那道题,会发现这个题相当于哈密顿的弱化版(此题没有操作二维数组,所以比较好写一点)。在有了哈密顿的经验之后,操作栈变得更加熟练了。这个题我最开始的确是写了标记数组标记,之后在回溯的时候再消去标记,结果写着写着发现居然只写了注释,没写相应的代码。。发现只有一个输出之后竟然用了十几分钟才发现这里错了。测试debug用n=2或者3就够了,数据大了是给自己添麻烦。推荐过了这个题之后再做做哈密顿那道题,会比较顺利。纠正一个自己之前的迷惑点:之前以为31号寄存器每进入一个新的标签,都会自动改变一下,其实这是不对的。只有jal能改动31号寄存器的值(当时就是纠结这个才会哈密顿写了6小时,不过最后弄明白就很好了)

另外感觉全排列比汉诺塔更适合在大一程设中当做例题讲,汉诺塔很容易把不明白函数传参的都搞晕了,而全排列只传递一个参数,不会卡在这里。不妨压一波P2课上测试考汉诺塔?可能不让输出路径只让输出方案数(推规律用通项公式水过去的统统问答时打死)课下有时间还是自己试试吧,竞赛+运动会完事儿了时间多的一匹,是补作业和肝计组的好时候了。去年好像还考了矩阵快速幂,也可以课下准备准备快速幂取模和矩阵快速幂,操作二维数组应该是比较常用且在考察范围内的知识点吧。

P2之前一定要看一遍preview里的教程,不能再在问答环节尴尬了。

01迷宫:

这个题是二维数组+dfs的题目,是我考场上不敢碰的题(一道一个半小时,不加debug)递归终止条件是到终点。递归深入的条件是:某个方向走完之后没越界,且是0。每次深入之前,先把要到的那个位置在原来的矩阵中修改为1(即以后不可以走),然后再dfs,最后再把原来矩阵修改回去。注意起始点先标记上

可能遇到的困难:在做这道题之前,室友和我讨论了一下关于在递归中写多个if语句如何实现跳回去再调回来的功能,以保证所有的if都可以执行一次,我依托宏封装基本操作以方便编程,把判断条件单独放入一个标签judge,维护栈并用jal跳转至judge进行判断,最后jr跳回来正好是下一个if语句,解决了这个问题。光说可能不太清楚,下面提供一下汇编程序实现01迷宫,关注dfs,dfswork,judge,back四个标签(仅供自己留备份+其他同学学习参考!严禁抄袭!提交一定会被查重)

 

C++
#include <iostream> #include <bits/stdc++.h> int matrix[20][20]; int n,m,sx,sy,ex,ey; int ans=0; typedef struct { int dx; int dy; }delta; delta d[4]={{0,-1},{0,1},{-1,0},{1,0}}; //高级语言中应该采取的走迷宫的写法 //在dfs中for(i=0;i<4;i++){遍历方向} void dfs(int x,int y); int overflow(int x,int y,int dx,int dy); int main() { scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ scanf("%d",&matrix[i][j]); } } scanf("%d %d",&sx,&sy); scanf("%d %d",&ex,&ey); matrix[sx][sy]=1; dfs(sx,sy); printf("%d\n",ans); return 0; } void dfs(int x,int y){ if(x==ex && y==ey){ ans++; return; }//考虑到mips中多开数组或者用结构体在规定时间内编码难以接受,且为了翻译方便,所以没有用循环和方向数组 //事实上应该声明一个方向结构体数组,存储下来所有方向,然后写一个循环跑所有方向,这样节省在高级语言中的代码量 if(!overflow(x,y,0,-1) && !matrix[x][y-1]){ matrix[x][y-1]=1; dfs(x,y-1); matrix[x][y-1]=0; } if(!overflow(x,y,0,1) && !matrix[x][y+1]){ matrix[x][y+1]=1; dfs(x,y+1); matrix[x][y+1]=0; } if(!overflow(x,y,-1,0) && !matrix[x-1][y]){ matrix[x-1][y]=1; dfs(x-1,y); matrix[x-1][y]=0; } if(!overflow(x,y,1,0) && !matrix[x+1][y]){ matrix[x+1][y]=1; dfs(x+1,y); matrix[x+1][y]=0; } } int overflow(int x,int y,int dx,int dy){ if(x+dx<=n && x+dx>=1 && y+dy<=m && y+dy>=1) return 0; return 1; }
  1 .data
  2 matrix:    .space 400
  3 .macro readint(%ans) #常用简单操作写成宏比较方便
  4     li    $v0,5
  5     syscall
  6     move    %ans,$v0
  7 .end_macro
  8 .macro save(%a)        #入栈
  9     sw    %a,($sp)
 10     addi    $sp,$sp,-4
 11 .end_macro
 12 .macro load(%a)        #出栈
 13     addi    $sp,$sp,4
 14     lw    %a,($sp)
 15 .end_macro
 16 .text
 17     readint($s0)        #s0是n
 18     readint($s1)        #s1是m
 19     li    $s7,0        #s7是方案数
 20     li    $t0,1        #t0是i
 21 read_matrix:
 22     bgt    $t0,$s0,read_matrix_end
 23     li    $t1,1
 24 read:
 25     bgt    $t1,$s1,read_end
 26     addi    $t2,$t0,-1
 27     mult    $t2,$s1
 28     mflo    $t2        #t2=(i-1)*m
 29     add    $t2,$t2,$t1    #t2=(i-1)*m+j
 30     sll    $t2,$t2,2
 31     la    $s2,matrix
 32     add    $t2,$s2,$t2    #t2是在矩阵中的位置
 33     li    $v0,5
 34     syscall    
 35     sw    $v0,($t2)    #数据存储进入矩阵
 36     addi    $t1,$t1,1
 37     j    read
 38 read_end:
 39     addi    $t0,$t0,1
 40     j    read_matrix
 41 read_matrix_end:
 42     readint($s2)        #s2,s3,s4,s5分别是起始行列,终点行列
 43     readint($s3)
 44     readint($s4)
 45     readint($s5)
 46     
 47     addi    $t0,$s2,-1
 48     mult    $t0,$s1
 49     mflo    $t0
 50     add    $t0,$t0,$s3
 51     sll    $t0,$t0,2
 52     la    $t1,matrix
 53     add    $t0,$t0,$t1
 54     li    $t2,1
 55     sw    $t2,($t0)    #把出发点的位置标记为1,然后再dfs(最开始忘了,所以最终得到的方案数比预计的多)
 56     
 57     move    $a0,$s2
 58     move    $a1,$s3        #dfs参数,即目前的行和列
 59     jal    dfs    
 60     
 61     li    $v0,1
 62     move    $a0,$s7
 63     syscall
 64     li    $v0,10
 65     syscall
 66     
 67     
 68 dfs:                #dfs写的是返回条件,dfswork里面才写了递归调用        
 69     beq    $a0,$s4,hope
 70     j    dfswork
 71 hope:
 72     beq    $a1,$s5,ansadd
 73     j    dfswork
 74 ansadd:
 75     addi    $s7,$s7,1
 76     jr    $31
 77     
 78 #多个if语句的跳转的一种写法:为了能让多个if语句都能执行,并不能直接使用beq等指令,一种实现方法是用jal连接下一步并跳转到一个专门用于判断的过程judge中去
 79 #由于jal跳转,所以需要维护栈,如果用宏来封装维护栈的过程的话,这里就特别容易写(推荐把读入数据,维护栈这种常用简单操作封装,至于其他用到额外寄存器的操作,请谨慎封装)
 80 dfswork:                
 81     save($a0)
 82     save($a1)
 83     save($31)        #第一次进时,这里存的是回主函数的地址
 84     addi    $a0,$a0,0
 85     addi    $a1,$a1,-1
 86     jal    judge
 87     load($31)
 88     load($a1)
 89     load($a0)         #完成对于一个方向的判断
 90     
 91     save($a0)
 92     save($a1)
 93     save($31)
 94     addi    $a0,$a0,0
 95     addi    $a1,$a1,1
 96     jal    judge
 97     load($31)
 98     load($a1)
 99     load($a0)
100     
101     save($a0)
102     save($a1)
103     save($31)
104     addi    $a0,$a0,-1
105     addi    $a1,$a1,0
106     jal    judge
107     load($31)
108     load($a1)
109     load($a0)
110     
111     save($a0)
112     save($a1)
113     save($31)
114     addi    $a0,$a0,1
115     addi    $a1,$a1,0
116     jal    judge
117     load($31)
118     load($a1)
119     load($a0)
120     
121     jr    $31        #返回上一个dfs
122     
123 judge:
124     bgt    $a0,$s0,back    #如果行严格大于行数,返回
125     ble    $a0,$0,back    #如果行号小于等于0,返回
126     bgt    $a1,$s1,back    #同理
127     ble    $a1,$0,back
128     addi    $t0,$a0,-1    #由于存的时候就是这样的,所以此处也先减一
129     mult    $t0,$s1
130     mflo    $t1
131     add    $t1,$t1,$a1
132     sll    $t1,$t1,2
133     la    $t2,matrix
134     add    $t1,$t1,$t2    
135     lw    $t3,($t1)    #t3是矩阵中该位置的值,表示是否能走
136     bne    $t3,$0,back    #如果t3不能走,回退一次
137     li    $t4,1
138     sw    $t4,($t1)    #标记为不能走了
139     save($31)        #第一次进时,这里存的是judge下一条的地址
140     save($t1)
141     save($t2)
142     save($t3)
143     save($t4)        #不管哪些需要存哪些不需要存了,xjb存就完事了(其实这里只需要$t1存起来),最开始我$t1忘了存了
144     jal    dfs
145     load($t4)
146     load($t3)
147     load($t2)
148     load($t1)
149     load($31)
150     sw    $0,($t1)        #把值修改回去,以后又可以走了
151     jr    $31        #回judge的下一条指令,即判断另一个方向
152     
153     
154 back:
155     jr    $31

 

 

补充题:

选择排序:

当时没照着视频写,现在拿出来写了一下,发现没必要像教程中一样弄个global main(也不让用),直接写即可。因为怕错+自己太弱,所以先写了C代码,然后翻译的。比视频中写的短,不到70行。当然,有一定的规矩终究是好的,但从助教口中得知,就P2的难度来说,倒没必要这么搞。

矩阵快速幂:

先放出C++代码,注意取模取多了也会变慢而超时。////更新:快速幂的汇编代码考前更新不出来了,只是又写了一遍C的代码,保证如果碰到的话能快速写出正确代码,并做好了测试数据。

#include <bits/stdc++.h> 
#define ll    long long 
//矩阵快速幂的快速之处不在于加速两个矩阵相乘时内部的计算速度
//而是减少矩阵之间做乘法的次数 
ll mod=1000000007;
typedef struct{
    ll    a[200][200];
}Matrix;
Matrix m,ans;
void power(ll n,ll k);
Matrix mult(Matrix ans,Matrix base,ll n);
int main() {
    ll n,k;
    scanf("%lld %lld",&n,&k);
    for(ll i=0;i<n;i++){
        for(ll j=0;j<n;j++){
            scanf("%lld",&m.a[i][j]);
        }
    }
    power(n,k);
    for(ll i=0;i<n;i++){
        for(ll j=0;j<n;j++){
            printf("%lld ",ans.a[i][j]);
        }
        printf("\n");
    }
    return 0;
}
void power(ll n,ll k){
    Matrix base=m;
    for(ll i=0;i<n;i++){
        ans.a[i][i]=1; //构造单位矩阵 
    }
    while(k>0){
        if(k&1){
            ans=mult(ans,base,n);
        }
        base=mult(base,base,n);
        k>>=1;
    }
}
Matrix mult(Matrix ans,Matrix base,ll n){
    Matrix result;
    for(ll i=0;i<n;i++){
        for(ll j=0;j<n;j++){
            result.a[i][j]=0;
        }
    }
    for(ll i=0;i<n;i++){
        for(ll j=0;j<n;j++){
            for(ll k=0;k<n;k++){
                result.a[i][j]+=(((ans.a[i][k])*(base.a[k][j]))%mod);
//              result.a[i][j]+=((ans.a[i][k]%mod)*(base.a[k][j]%mod)%mod);会慢,超时 
                result.a[i][j]%=mod;
            }
        }
    }
    return result;
}

 

汉诺塔:

理解用汇编语言写递归的本质:把所有的参数都存在堆栈里,然后进下一层,最后递归终止时输出一行移动盘子。参考的去年OJ上的输出要求。

#include <stdio.h>
#include <stdlib.h>
void dfs(int n,char from,char tmp,char to);
int main() {
    int n;
    char from='A',tmp='B',to='C';
    scanf("%d",&n);
    dfs(n,from,tmp,to);
    
    return 0;
}
void dfs(int n,char from,char tmp,char to){
    if(n==1){
        printf("%c --> %c\n",from,to);
        return;
    }
    dfs(n-1,from,to,tmp);
    dfs(1,from,tmp,to);
    dfs(n-1,tmp,from,to);
    return;
}

下面是递归程序,比较容易写

 1 .data
 2 connect:    .asciiz " --> "
 3 enter:    .asciiz "\n"
 4 _a:    .asciiz "A"
 5 _b:    .asciiz "B"
 6 _c:    .asciiz "C"
 7 .macro save(%a)
 8     sw    %a,($sp)
 9     addi    $sp,$sp,-4
10 .end_macro
11 
12 .macro swap(%a,%b)
13     move    $t0,%a
14     move    %a,%b
15     move    %b,$t0
16 .end_macro
17 
18 .macro load(%a)
19     addi    $sp,$sp,4
20     lw    %a,($sp)
21 .end_macro
22 
23 .text
24     li    $v0,5
25     syscall
26     move    $s0,$v0        #s0是n
27     move    $a0,$v0
28     la    $a1,_a
29     la    $a2,_b
30     la    $a3,_c        #配置四个参数:盘子数,from,tmp,to
31     jal     dfs
32     
33     li    $v0,10
34     syscall
35 
36 dfs:
37     bgt    $a0,1,dfs_work
38     move    $a0,$a1
39     li    $v0,4
40     syscall
41     la    $a0,connect
42     li    $v0,4
43     syscall
44     move    $a0,$a3
45     li    $v0,4
46     syscall
47     la    $a0,enter
48     li    $v0,4
49     syscall
50     jr    $31
51     
52 dfs_work:
53     save($31)
54     save($a0)
55     save($a1)
56     save($a2)
57     save($a3)
58     addi    $a0,$a0,-1
59     swap($a2,$a3)
60     jal    dfs
61     load($a3)
62     load($a2)
63     load($a1)
64     load($a0)
65     load($31)
66     
67     save($31)
68     save($a0)
69     save($a1)
70     save($a2)
71     save($a3)
72     li    $a0,1
73     jal    dfs
74     load($a3)
75     load($a2)
76     load($a1)
77     load($a0)
78     load($31)
79     
80     save($31)
81     save($a0)
82     save($a1)
83     save($a2)
84     save($a3)
85     addi    $a0,$a0,-1
86     swap($a1,$a2)
87     jal    dfs
88     load($a3)
89     load($a2)
90     load($a1)
91     load($a0)
92     load($31)
93     
94     jr    $31
95     

 课上测试部分:

第一题:阶乘求和,给定数n,求1!+2!+…………+n! 这个实现起来还是挺简单的,毕竟二维数组和递归都能熟练操作了,二重循环不算啥了。

第二题:单步汉诺塔,几乎压中。写过普通汉诺塔的那个题的汇编程序的话,这个题也就是十分钟吧。题目做出的改动是:只能相邻柱子之间移动。还好给了C代码,翻译起来和普通汉诺塔如出一辙。追根溯源可知,该题对应这里:https://accoding.cn/problem/1073/index 是17级的一道程设题,上个星期还和室友讨论过……考场遇到研究过的问题是真的爽。

第三题:高精度乘法,碰巧我也准备了C的代码,所以也不会费太多的事。这个题可能卡人的地方可能有同学不会写高精度乘法。不巧我之前的一篇博客介绍过……大家可以移步那里。

posted @ 2019-10-26 23:17  BUAA-Wander  阅读(2791)  评论(4编辑  收藏  举报