Try   HackMD

2019q3 Homework2 (quiz2)

contributed by < uccuser159 >

題目1

  • [ ]考慮下方檔案 4thought.c 是 ACM-ICPC 題目 4 thought 的一個解法,假設程式的輸入符合 4 thought 的描述,請補完程式碼:
#include <stdbool.h>
#include <stdio.h>

enum {
    opType1 = 0x1 << 0, opType2 = 0x1 << 1,
    opType3 = 0x1 << 4, opType4 = 0x1 << 5,
};

static int operate(int op, int a, int b) {
    switch (op) {
    case opType1: return a + b;
    case opType2: return a - b;
    case opType3: return a * b;
    case opType4: return (int) a / b;
    }
    return 0;
}

static char op_to_char(int op) {
    return "+-*/?"[op - 1];
}
static int op_to_prio(int op) {
    return ((int[]){opType1, opType2, opType3, opType4, -1})[op - 1];
}

static int calc(int op1, int op2, int op3) {
    op1 = op_to_prio(op1);
    op2 = op_to_prio(op2);
    op3 = op_to_prio(op3);

    bool p1 = (op1 & 0x0F) == 0;  // = 1 for * or /
    bool p2 = (op2 & 0x0F) == 0;  // else = 0
    bool p3 = (op3 & 0x0F) == 0;

    // (4 + 4 + 4 + 4) or (4 / 4 / 4 / 4)
    if ((p1 == p2) && (p2 == p3))
        return operate(op3, operate(op2, operate(op1, 4, 4), 4), 4);
    /* Write your code here */

    return 0;
}

int main(void) {
    int n;
    scanf("%d", &n);
    int sol[n];

    for (int i = 0; i < n; i++)
        scanf("%d", &sol[i]);

    bool validSolution = false;
    for (int i = 0; i < n; i++) {
        for (int op1 = 4; op1 > 0; op1--) {
            for (int op2 = 4; op2 > 0; op2--) {
                for (int op3 = 4; op3 > 0; op3--) {
                    int sol_checked = calc(op1, op2, op3);
                    if (sol_checked == sol[i]) {
                        validSolution = true;

                        char op1char = op_to_char(op1);
                        char op2char = op_to_char(op2);
                        char op3char = op_to_char(op3);

                        printf("4 %c 4 %c 4 %c 4 = %d\n", op1char, op2char,
                               op3char, sol[i]);
                        op1 = -1; op2 = -1; op3 = -1;
                        break;
                    }
                }
            }
        }
        if (!validSolution)
            printf("no solution\n");
        validSolution = false;
    }
    return 0;
}

思考

calc函式中式在做四則運算,考慮先乘除後加減的問題:
4 __ 4 __ 4 __ 4 (底線處放運算子 + . - . x . /)
根據op_to_prio函式回傳的值與 0x0F 做 AND 運算:
(+ . -)應該得到的布林值為0; (x . /)得到的布林值為1
所以應該補入的程式碼為:

if(!p1 && p2 && p3) // 4 + 4 x 4 x 4
    return operate(op1,(operate(op3,operate(op2,4,4),4),4); 
if(!p1 && !p2 && p3) // 4 + 4 + 4 x 4
    return operate(op2,(operate(op1,operate(op3,4,4),4),4);
if(!p1 && p2 && !p3) // 4 + 4 x 4 + 4
    return operate(op3,(operate(op1,operate(op2,4,4),4),4);
if(p1 && !p2 && p3) // 4 x 4 + 4 x 4
    return operate(op2,(operate(op3,operate(op1,4,4),4),4);
if(p1 && p2 && !p3) // 4 x 4 x 4 + 4
    return operate(op3,(operate(op2,operate(op1,4,4),4),4);
  • 延伸問題

題目2

  • [ ]考慮以下程式碼 (fitbits.c) 可檢驗輸入的整數 x 是否可用 n 個位元來表示,例如 (x = 4, n = 9) 要回傳 true, 當 (x = 4, n = 2) 回傳 false
#include <stdbool.h>
bool fit_bits(int x, int n) {
    /* Write your code here */
    return (bool) x;
}

實作的程式碼不能有任何邏輯條件判斷 (如 if, else, ?) 或迴圈 (如 for, while, goto, switch, case, break, continue),可用的運算子是 >>, <<, -, +, !, ~, &, |

請補完程式碼,作答時需要包含函式宣告及定義。除了撰寫程式,你應該提供對應的程式碼註解。


思考

若 n=3 只要小於8的數字皆可以用3個位元表示;若 n=4 只要小於16的數字皆可以用4個位元表示,以此類推,要數字 x 可以被 n 個位元表示,則

x<=2n
所以補完的程式碼為:

#include <stdbool.h>
bool fit_bits(int x, int n) {
    x = ((x >> n) == 0); 
    //將數字 x 除以2且取整數 n 次,若為0則可以被 n 位元表示,反之則不能被 n 位元表示
    return (bool) x;
}
  • 延伸問題

題目3

  • [ ]考慮以下程式碼 (is-less-equal.c) 可檢驗輸入的整數 xy,是否存在
    x<=y
    的關係。例如 (x = 4, y = 4) 要回傳 true, 當 (x = 14, y = 9) 回傳 false
#include <stdbool.h>
bool is_leq(int x, int y) {
     int s;
    /* Write your code here */
    return (bool) s;
}

實作的程式碼不能有任何邏輯條件判斷 (如 if, else, ?) 或迴圈 (如 for, while, goto, switch, case, break, continue),當然也不能用 >=, >, <, <=, - 等運算子。可用的運算子是 >>, <<, +, ~

請補完程式碼,作答時需要包含函式宣告及定義。除了撰寫程式,你應該提供對應的程式碼註解。


思考

用兩數相減來比較大小,若 s = y - x >= 0,則 s 的 MSB 為0;若 s = y - x < 0,則 s 的 MSB 為1。此處不能使用 "-" 運算子,故採用取2補數的減法。
所以補完的程式碼為:

#include <stdbool.h>
bool is_leq(int x, int y) {
    int s;
    s = y + (~x+1); // s = y - x
    s = ((s >> 31) == 0); // 判斷 s 的正負(s>=0回傳true;s<0回傳false)
    return (bool) s;
}
  • 延伸問題

題目4

考慮一種針對短字串的最佳化操作,假設字串總是小於等於 8 bytes,標的硬體是像 x86_64 這樣的 64-bit 架構而且是 little endian,於是我們可實作類似以下的程式碼 (ministr.c):

#include <stdint.h>
#include <stdio.h>
#include <string.h>

typedef union {
    uint64_t integer;
    char array[8];
} mini_str;

static unsigned BitScanReverse(uint64_t x) {
    return 63 - __builtin_clzll(x);
}

/**
 * Find the length of the given mini_str.
 * @param str string to find length of
 * @return length of the given string
 */
unsigned mini_strlen(mini_str str) {
    // Special case for empty string.
    if (str.integer == 0) return 0;

    // Otherwise find first non-zero bit (which will be in the first non-zero
    // byte), and find which byte it is in.
    // FIXME: Assumes little-endian.
    unsigned msb = BitScanReverse(str.integer);
    return msb / 8 + 1;
}

/**
 * Create a new mini_str with length 0.
 * @return newly created mini_str
 */
mini_str mini_str_new(void) {
    // Create string of all null bytes.
    mini_str str = {.integer = 0};
    return str;
}

/**
 * Append str2 to the end of str1 and return the reult.
 * @param str1 first string
 * @param str2 second string
 * @return combined string
 */
mini_str mini_strcat(mini_str str1, mini_str str2) {
    // Shift str2 along by str1Len characters to move it into position.
    unsigned str1Len = mini_strlen(str1);
    str2.integer <<= str1Len * 8;  // FIXME: Assumes little-endian.
    
    /* Write your code here */
    return str1;
}

#define mini_str_to_c(mini_str) ((const char *) (mini_str).array)
#define mini_str_to_cNoConst(mini_str) ((char *) (mini_str).array)

/**
 * Create a mini_str from a standard C character array.
 * @param cstr Null-terminated C-string to use as input
 * @return newly created mini_str
 */
mini_str mini_str_from_c(const char *cstr) {
    // Create empty string.
    mini_str mini_str = mini_str_new();

    // Copy string.
    strncpy(mini_str_to_cNoConst(mini_str), cstr, 7);

    return mini_str;
}

int main(int argc, char **argv) {
    mini_str all = mini_str_from_c("All ");
    mini_str red = mini_str_from_c("red");

    mini_str cat = mini_strcat(all, red);
    printf("%s\n", mini_str_to_c(cat));

    return 0;
}

這裡的 __builtin_clzll 是 GCC builtin function,作用是 bit scan,程式預期輸出為:

All red

你應該要實作 calc 函式中標註 /* Write your code here */ 之後的程式碼。除了撰寫程式,你應該提供對應的程式碼註解。
注意: 實作的程式碼不能有任何邏輯條件判斷 (如 if, else, ?) 或迴圈 (如 for, while, goto, switch, case, break, continue),也不能用 >=, >, <, <=, - 等運算子。


思考

mini_strcat函式要合併兩個mini_strstr1Lenstr1的長度。
tr2.integer <<= str1Len * 8 是為了要騰出str1長度的字串空間,再來就是要把str2補進去str1
所以補完的程式碼為:

/**
 * Append str2 to the end of str1 and return the reult.
 * @param str1 first string
 * @param str2 second string
 * @return combined string
 */
mini_str mini_strcat(mini_str str1, mini_str str2) {
    // Shift str2 along by str1Len characters to move it into position.
    unsigned str1Len = mini_strlen(str1);
    str2.integer <<= str1Len * 8;  // FIXME: Assumes little-endian.
    str1.integer += str2.integer; // append str2
    return str1;
}
  • 延伸問題

修改成適用 big/little-endian
GCC builtin function 中,可以查到 Built-in Function:

int __builtin_clz (unsigned int x)
Returns the number of leading 0-bits in x, starting at the most significant bit position. If x is 0, the result is undefined.
int __builtin_clzll (unsigned long long)
Similar to __builtin_clz, except the argument type is unsigned long long.

題目5 & 6

  • [ ]population count 簡稱 popcount 或叫 sideways sum,是計算數值的二進位表示中,有多少位元是 1,在一些場合下很有用,例如計算 0-1 稀疏矩陣 (sparse matrix)或 bit array 中非 0 元素個數、計算兩個字串的 Hamming distance。Intel 在 2008 年 11 月 Nehalem 架構的處理器 Core i7 引入 SSE4.2 指令集,其中就有 CRC32POPCNT 指令,POPCNT 可處理 16-bit, 32-bit, 64-bit 整數。

GCC 提供對應的 builtin function:

__builtin_popcount(x): x 總共有幾個 1。使用示範:

int x = 5328; // 00000000000000000001010011010000
printf("%d\n",  __builtin_popcount(x)); // 5

以下是個存在實作缺陷的版本:

int popcnt_naive(int n) {
    int count = 0;
    while (n) {
        if (n & 1)
            ++count;
        n = n >> 1;
    }
    return count;
}

呼叫 popcnt_naive(-1) 時,會造成無窮迴圈,請指出錯誤所在,並且重寫為正確的版本。


思考

因為(-1)=11111112(32 bits)且當在做算術右移時,會一直補 MSB 進來,所以用函式popcnt_naive(int n) ,只要參數 n 小於0,即會造成無窮迴圈。
所以改進的程式碼為:

int popcnt(int n) {
    int count = 0;
    while (n) {
    n = n && (n-1); // remove the right most 1
    count = count + 1;
    }
    return count;
}
  • 延伸測驗 5,實作 branchless 的 popcnt 並附上對應的程式原理解說。
int popcnt_branchless(int x){
    x = (0x55555555 & x) + (0x55555555 & (x>> 1));  // 0-2 in 2 bits
    x = (0x33333333 & x) + (0x33333333 & (x>> 2));  // 0-4 in 4 bits
    x = (0x0f0f0f0f & x) + (0x0f0f0f0f & (x>> 4));  // 0-8 in 8 bits
    x = (0x00ff00ff & x) + (0x00ff00ff & (x>> 8));  // 0-16 in 16 bits
    x = (0x0000ffff & x) + (0x0000ffff & (x>>16));  // 0-32 in 32 bits
    return x;
}

題目7