在刷pat的1073 多選題常見計分法題目時,發現如果需要判斷每一個學生對應每道題的多選題是否錯選,漏選,以及選對是比較麻煩的一件事,因為這涉及到兩個集合的判斷,判斷一個集合是否是另一個集合的子集(即漏選,得一半的分),或者說兩個集合是否完全相等(即題目得滿分)。
剛開始通過set容器來保存每一道題的正確答案,以及學生選擇的答案,然后比較兩個集合的大小,大小一致則for循環判斷每一個元素是否都存在。結果發現這種思路過於復雜,且易超時。
聯想到每一個選項是否一致,可以通過異或運算判斷兩個集合,如果結果為0則得滿分,否則就是錯選或者漏選,錯選漏選通過或運算來判斷是否能夠得到正確集合,如果可以則是漏選,如果不能則說明是錯選。
在完整的實現這道題之前,先來學習一下位運算的基礎。
1.常見的位運算
常見的位運算有6個,見如下表格:
| Operators | Meaning of operators |
|---|---|
| & | Bitwise AND,按位與 |
| | | Bitwise OR,按位或 |
| ^ | Bitwise XOR,按位異或 |
| ~ | Bitwise complement,按位取反 |
| << | Shift left,左移 |
| >> | Shift right,右移 |
1.1按位與
運算舉例,對12和25進行按位與操作:
12 = 00001100 (In Binary)
25 = 00011001 (In Binary)
Bit Operation of 12 and 25
00001100
& 00011001
________
00001000 = 8 (In decimal)
當且僅當,兩個二進制位都為1時,結果才為1
代碼舉例:
#include <stdio.h>
int main()
{
int a = 12, b = 25;
printf("Output = %d", a&b);
return 0;
}
Output:
Output = 8
1.2按位或
運算舉例,對12和25進行按位或操作:
12 = 00001100 (In Binary)
25 = 00011001 (In Binary)
Bitwise OR Operation of 12 and 25
00001100
| 00011001
________
00011101 = 29 (In decimal)
當且僅當,兩個二進制位都為0時,結果才為0,其它情況都為1
代碼舉例:
#include <stdio.h>
int main()
{
int a = 12, b = 25;
printf("Output = %d", a|b);
return 0;
}
Output:
Output = 29
1.3按位異或
運算舉例,對12和25進行按位異或操作:
12 = 00001100 (In Binary)
25 = 00011001 (In Binary)
Bitwise XOR Operation of 12 and 25
00001100
^ 00011001
________
00010101 = 21 (In decimal)
當且僅當,兩個二進制位相異時,結果才為1,其它情況都為0
代碼舉例:
#include <stdio.h>
int main()
{
int a = 12, b = 25;
printf("Output = %d", a^b);
return 0;
}
Output:
Output = 21
對於異或運算的理解
- 找出兩個數有差異的位,a^b得到的結果中,1表示在該位兩數存在差別,0表示無差別,這個很好理解
- 將一個數按照另一個數的對應位的取值改變取值,如ab(1000101000110011),可以看成a按照b的要求改變對應位的取值(1為改變,0為不改變)故得到10111001
1.4按位取反
運算舉例,對35進行按位取反操作:
35 = 00100011 (In Binary)
Bitwise complement Operation of 35
~ 00100011
________
11011100 = 220 (In decimal)
代碼舉例:
#include <stdio.h>
int main()
{
printf("Output = %d\n",~35);
printf("Output = %d\n",~-12);
return 0;
}
Output:
Output = -36
Output = 11
為什么這里35按位取反的結果不是220,而是-36。
對於任何整數n,n的按位取反將為-(n + 1)。要了解這一點,需要了解二進制的補碼表示
十進制 二進制 二進制補碼
0 00000000 -(11111111+1) = -00000000 = -0(decimal)
1 00000001 -(11111110+1) = -11111111 = -256(decimal)
12 00001100 -(11110011+1) = -11110100 = -244(decimal)
220 11011100 -(00100011+1) = -00100100 = -36(decimal)
35的按位補碼為220(十進制)。 220的2的補碼是-36。因此,輸出是-36而不是220。
1.5移位運算
1.左移
可以簡單理解為*2,對比十進制中的左移,比如10進制的13左移1位得到130,所以
二進制中的13左移1位得到26
左移n位,結果就是乘以2的n次方
1101
<<1
11010
十進制為26
2.右移
類比左移,右移就是除以2
代碼舉例:
#include <stdio.h>
int main()
{
int num=212, i;
for (i=0; i<=2; ++i)
printf("Right shift by %d: %d\n", i, num>>i);
printf("\n");
for (i=0; i<=2; ++i)
printf("Left shift by %d: %d\n", i, num<<i);
return 0;
}
Output
Right Shift by 0: 212
Right Shift by 1: 106
Right Shift by 2: 53
Left Shift by 0: 212
Left Shift by 1: 424
Left Shift by 2: 848
2.解題思路
對於每一個選項,我都可以通過二進制來表示出來,比如
a--00001
b--00010
c--00100
d--01000
e--10000
//因為選項個數在[2,5]區間,所以最大選項就是e
這樣的話,通過兩個集合(集合A={所有的正確選項的二進制表示的或運算結果},集合B={所有學生的選項的二進制表示的或運算結果})
比如
A=10001,即正確的選項為ae
B=10000,即學生的選項為e
第一步對A和B進行異或運算,
-
如果結果為0,說明滿分
-
如果結果不為0,說明存在選項不一致,可能漏選,可能錯選
第二步對A和B進行或運算,
- 如果結果為A,說明B就是漏選的,得分50%
- 否則就是有錯選,不得分
第三步對A和B的異或結果和{1,2,4,8,16}集合中的元素分別進行與運算,判斷當前題目,學生選錯的選項是哪一個
比如:正確選項是10011,學生的答案是01100,異或結果為11111,對異或結果11111和1,2,4,8,16分別進行與運算,比如11111&00001結果不為零,則說明該選項是錯誤的,以此類推,循環進行與運算,得出學生選擇的選項都是錯誤的。正確的選項都沒有選,所以也記為錯選選項。
通過異或運算和或運算以及與運算來判斷全選對,漏選,錯選以及對應的錯誤選項就簡單多了
3.代碼實現
#include<iostream>
#include<vector>
using namespace std;
int main() {
//1.保存所有的題目信息
int n,m;
scanf("%d%d",&n,&m);
//a=00001=1
//b=00010=2
//c=00100=4
//d=01000=8
//e=10000=16
int hash[5] = {1,2,4,8,16} , trueopt[m]= {0};
int fullscore[m];
//1.記錄每道題的總分到fullscore數組,每道題的正確選項到trueopt
for(int i=0; i<m; i++) {
int tmpscore,tmpalloptsize,tmprightoptsize;
scanf("%d%d%d",&tmpscore,&tmpalloptsize,&tmprightoptsize);
fullscore[i] = tmpscore;
for(int j=0; j<tmprightoptsize; j++) {
char tmpopt;
scanf(" %c",&tmpopt);
trueopt[i] +=hash[tmpopt-'a'];
}
}
//記錄每道題每個選項的出錯次數
vector<vector<int>> cnt(m,vector<int>(5));
//2.計算每個學生的分數,並保存錯誤選項出錯次數到cnt中
for(int i=0; i<n; i++) {
double stuscore = 0;
for(int j=0; j<m; j++) {
getchar();
int k;
scanf("(%d",&k);
int selectedopt = 0;
for(int o=0; o<k; o++) {
char tmpc;
scanf(" %c",&tmpc);
selectedopt+=hash[tmpc-'a'];
}
scanf(")");
//計算異或結果
int result = selectedopt^trueopt[j];
if(result) {
//不為零,有漏選或錯選,進行或運算
int huo = selectedopt|trueopt[j];
if(huo == trueopt[j]) {
//漏選
stuscore += fullscore[j]*1.0/2;
}
if(result) {
//錯選,不得分,記錄錯誤選項
for (int k = 0; k < 5; k++)
if (result & hash[k]) cnt[j][k]++;
}
} else {
//滿分
stuscore += fullscore[j];
}
}
printf("%.1f\n",stuscore);
}
//循環遍歷cnt錯誤選項最多的
int maxcnt =0;
for(int i=0; i<m; i++) {
for(int j=0; j<cnt[i].size(); j++) {
maxcnt = cnt[i][j]>maxcnt?cnt[i][j]:maxcnt;
}
}
if (maxcnt == 0) {
printf("Too simple\n");
} else {
for (int i = 0; i < m; i++) {
for (int j = 0; j < cnt[i].size(); j++) {
if (maxcnt == cnt[i][j])
printf("%d %d-%c\n", maxcnt, i+1, 'a'+j);
}
}
}
return 0;
}
