C brush question: LeetCode 752 Open the turntable lock (medium) | BFS wide search detailed explanation

Posted by Christian B. on Mon, 17 Jan 2022 15:17:37 +0100

Next blog: C brush question: LeetCode 752 Open the turntable lock (medium) | BFS wide search detailed explanation (1)

Hash version

Add HASH data structure, be familiar with the use of uthash, and add constraints.

Main constraints:

  • Do not turn back, for example, if you turn forward once, you are not allowed to turn back
  • Can't cross deands
  • Add the termination condition and return once the target is matched

Do not turn back, for example, if you turn forward once, you are not allowed to turn back. If the initial state is 0000, turn once to 1000, and turn again to 0000. The main idea of the implementation is to add a hash table visit to record all existing traversal results. If the same results appear, they will be skipped and not recorded. It mainly uses the O(1) search function of uthash hash table to reduce the search time.

You can't cross deands, which is also a hash table lookup idea similar to visit. The time complexity of the hash table is O(1), but it changes space for time. Reducing time will increase space at the same time.

The termination condition, because the idea itself extends outward from the rotation number 0, is similar to a greedy algorithm idea, and each step is optimal. Once the target is matched, the current rotation number is the smallest, so it can be returned directly.

The hash version code submitted is as follows:

#define STR_SIZE 5
#define STR_LEN 4
int g_curLevelCnt;

typedef struct HashTable {
    char str[STR_SIZE]; // key
    UT_hash_handle hh; // table head
} StruHashTable;

typedef struct QueList {
    int cnt; // Number of turns
    char *s; // Current password
    struct QueList *next; // Next possible password
} StruQueList, *PtrStruQueList;

char* AddOne(char *in, int j)
{
    char *res = (char *)malloc(sizeof(char) * STR_SIZE);
    if (res == NULL) {
        return NULL;
    }
    memcpy(res, in, STR_SIZE);
    char ch = res[j];
    if (ch == '9') {
        res[j] = '0';
        return res;
    }
    res[j] = ch + 1;
    return res;
}

char* MinusOne(char *in, int j)
{
    char *s = (char *)malloc(sizeof(char) * STR_SIZE);
    if (s == NULL) {
        return NULL;
    }
    memcpy(s, in, STR_SIZE);
    char ch = s[j];
    if (ch == '0') {
        s[j] = '9';
        return s;
    }
    s[j] = ch - 1;
    return s;
}

void Init(StruQueList **pQue, char *s, int cnt)
{
    (*pQue) = (PtrStruQueList)malloc(sizeof(StruQueList));
    (*pQue)->cnt = cnt;
    char *str = (char *)malloc(sizeof(char) * STR_SIZE);
    if (str == NULL) {
        return;
    }
    memcpy(str, s, STR_SIZE);
    (*pQue)->s = str;
    (*pQue)->next = NULL;
    g_curLevelCnt++;
}

// If it is greater than 0, the matching is successful and the number of turns is returned
// If it is equal to 0, there is no exception
// Less than 0 indicates an error
int NodeExpand(StruQueList **queList, StruQueList **ptrQueListLastNode, char *target, StruHashTable **ptrDead, StruHashTable **ptrVisit)
{
    int i;
    char *s;
    StruHashTable *hashTmp1, *hashTmp2;

    // Rotate once, and the current node evolves into 8 possibilities
    char *cur = (*queList)->s;
    int cnt = (*queList)->cnt;
    for (i = 0; i < 4; i++) {
        s = AddOne(cur, i);
        // If it matches the target
        if (strcmp(s, target) == 0) { // Termination conditions
            return cnt + 1;
        }

        // If in deands
        HASH_FIND(hh, *ptrDead, s, sizeof(char) * STR_SIZE, hashTmp1);
        // If traversed
        HASH_FIND(hh, *ptrVisit, s, sizeof(char) * STR_SIZE, hashTmp2);
        if (hashTmp1 == NULL && hashTmp2 == NULL) { // Not in dead, not in visit
            Init(&(*ptrQueListLastNode)->next, s, cnt + 1);
            *ptrQueListLastNode = (*ptrQueListLastNode)->next;
            hashTmp1 = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
            if (hashTmp1 == NULL) {
                return -1;
            }
            memcpy(hashTmp1->str, s, STR_SIZE);
            HASH_ADD(hh, *ptrVisit, str, sizeof(char) * STR_SIZE, hashTmp1);
            // printf("%s\n", s);
        } else {
            free(s);
        }

        s = MinusOne(cur, i);
        // If it matches the target
        if (strcmp(s, target) == 0) { // Termination conditions
            return cnt + 1;
        }
        // If in deands
        HASH_FIND(hh, *ptrDead, s, sizeof(char) * STR_SIZE, hashTmp1);
        // If traversed
        HASH_FIND(hh, *ptrVisit, s, sizeof(char) * STR_SIZE, hashTmp2);
        if (hashTmp1 == NULL && hashTmp2 == NULL) { // Not in dead, not in visit
            Init(&(*ptrQueListLastNode)->next, s, cnt + 1);
            *ptrQueListLastNode = (*ptrQueListLastNode)->next;
            hashTmp1 = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
            if (hashTmp1 == NULL) {
                return -1;
            }
            memcpy(hashTmp1->str, s, STR_SIZE);
            HASH_ADD(hh, *ptrVisit, str, sizeof(char) * STR_SIZE, hashTmp1);
            // printf("%s\n", s);
        } else {
            free(s);
        }
    }
    // printf("\n");
    return 0;
}

// According to the method of queue and BFS, it is expressed only once at a time, and all the corresponding possibilities
int openLock(char ** deadends, int deadendsSize, char * target)
{
    char cur[STR_SIZE] = "0000"; // Initial value
    int i, ret;

    // special case
    if (strcmp(cur, target) == 0) {
        return 0;
    }

    // Initialize hash
    StruHashTable *dead = NULL; // The header is empty at the beginning
    StruHashTable *hashTmp;
    for (i = 0; i < deadendsSize; i++) {
        HASH_FIND(hh, dead, deadends[i], sizeof(char) * STR_SIZE, hashTmp); // Space occupied by key value sizeof(char) * 5
        if (hashTmp == NULL) { // Not before
            hashTmp = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
            if (hashTmp == NULL) {
                return -1;
            }
            memcpy(hashTmp->str, deadends[i], STR_SIZE);
            HASH_ADD(hh, dead, str, sizeof(char) * STR_SIZE, hashTmp); // str represents the key value in the operation structure and is appended to hashtable
        }
    }

    // special case
    HASH_FIND(hh, dead, target, sizeof(char) * STR_SIZE, hashTmp);
    if (hashTmp != NULL) {
        return -1;  // deanends contain target
    }
    HASH_FIND(hh, dead, cur, sizeof(char) * STR_SIZE, hashTmp);
    if (hashTmp != NULL) {
        return -1;  // deanends contain target
    }

    // Initialize queue 0000
    StruHashTable *visit = NULL; // The header is empty at the beginning
    PtrStruQueList queList, queListCurLevelLast, queListLastNode;
    queList = (PtrStruQueList)malloc(sizeof(StruQueList));
    if (queList == NULL) {
        return -1; // if malloc is failed
    }
    g_curLevelCnt = 0;
    Init(&queList, cur, 0);
    hashTmp = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
    if (hashTmp == NULL) {
        return -1;
    }
    memcpy(hashTmp->str, queList->s, STR_SIZE);
    // printf("%p\n", visit);
    HASH_ADD(hh, visit, str, sizeof(char) * STR_SIZE, hashTmp); // Add a traversed state
    // printf("%p\n", visit);

    g_curLevelCnt = 0;
    queListLastNode = queList;
    ret = NodeExpand(&queList, &queListLastNode, target, &dead, &visit);
    if (ret > 0) { // Termination conditions
        return ret;
    }
    queListCurLevelLast = queListLastNode;
    queList = queList->next;

    // Iterate new possibilities from each node of all layers
    while (queList != NULL) {
        // Traverse all nodes of the current layer
        int len = g_curLevelCnt;
        g_curLevelCnt = 0;
        for (i = 0; i < len; i++) {
            ret = NodeExpand(&queList, &queListLastNode, target, &dead, &visit);
            if (ret > 0) { // Termination conditions
                return ret;
            }
            queList = queList->next;
        }
    }

    return -1; // Traverse all no matches
}

Hash version optimization

Code simplification: delete redundant information and refine duplicate code into independent sub functions.

  • Remove and merge duplicate codes to reduce redundancy
  • Split more than 50 lines of code into sub function functions
  • Replace the devil number with a meaningful macro name
  • Optimize the while loop plus lastNode to judge as a for loop, and use the global variable to count the number of current layers as len

The main change codes are as follows:

void InitDeadHash(char **deadends, int deadendsSize, StruHashTable **ptrDead)
{
    int i;
    StruHashTable *hashTmp;
    for (i = 0; i < deadendsSize; i++) {
        HASH_FIND(hh, *ptrDead, deadends[i], sizeof(char) * STR_SIZE, hashTmp); // Space occupied by key value sizeof(char) * 5
        if (hashTmp == NULL) { // Not before
            hashTmp = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
            if (hashTmp == NULL) {
                return;
            }
            memcpy(hashTmp->str, deadends[i], STR_SIZE);
            HASH_ADD(hh, *ptrDead, str, sizeof(char) * STR_SIZE, hashTmp); // str represents the key value in the operation structure and is appended to hashtable
        }
    }
    return;
}

void InitQueAndVisitHash(char *cur, StruQueList **ptrQueList, StruHashTable **ptrVisit)
{
    StruHashTable *hashTmp;
    *ptrQueList = (PtrStruQueList)malloc(sizeof(StruQueList));
    if (*ptrQueList == NULL) {
        return; // if malloc is failed
    }
    g_curLevelCnt = 0;
    Init(ptrQueList, cur, 0);
    hashTmp = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
    if (hashTmp == NULL) {
        return;
    }
    memcpy(hashTmp->str, (*ptrQueList)->s, STR_SIZE);
    HASH_ADD(hh, *ptrVisit, str, sizeof(char) * STR_SIZE, hashTmp); // Add a traversed state
    return;
}

int DealCurStr(char *s, char *target, int cnt, StruQueList **ptrQueListLastNode, StruHashTable **ptrDead, StruHashTable **ptrVisit)
{
    StruHashTable *hashTmp1, *hashTmp2;
    // If it matches the target
    if (strcmp(s, target) == 0) { // Termination conditions
        return cnt + 1;
    }

    // If in deands
    HASH_FIND(hh, *ptrDead, s, sizeof(char) * STR_SIZE, hashTmp1);
    // If traversed
    HASH_FIND(hh, *ptrVisit, s, sizeof(char) * STR_SIZE, hashTmp2);
    if (hashTmp1 == NULL && hashTmp2 == NULL) { // Not in dead, not in visit
        Init(&(*ptrQueListLastNode)->next, s, cnt + 1);
        *ptrQueListLastNode = (*ptrQueListLastNode)->next;
        hashTmp1 = (StruHashTable *)malloc(sizeof(StruHashTable)); // Add a hash node
        if (hashTmp1 == NULL) {
            return -1;
        }
        memcpy(hashTmp1->str, s, STR_SIZE);
        HASH_ADD(hh, *ptrVisit, str, sizeof(char) * STR_SIZE, hashTmp1);
        // printf("%s\n", s);
    } else {
        free(s);
    }
    return 0;
}

// If it is greater than 0, the matching is successful and the number of turns is returned
// If it is equal to 0, there is no exception
// Less than 0 indicates an error
int NodeExpand(StruQueList *queList, StruQueList **ptrQueListLastNode, char *target, StruHashTable **ptrDead, StruHashTable **ptrVisit)
{
    int i, ret;
    char *s;

    // Rotate once, and the current node evolves into 8 possibilities
    char *cur = queList->s;
    int cnt = queList->cnt;
    for (i = 0; i < 4; i++) {
        s = AddOne(cur, i);
        ret = DealCurStr(s, target, cnt, ptrQueListLastNode, ptrDead, ptrVisit);
        if (ret > 0) {
            return ret;
        }

        s = MinusOne(cur, i);
        ret = DealCurStr(s, target, cnt, ptrQueListLastNode, ptrDead, ptrVisit);
        if (ret > 0) {
            return ret;
        }
    }
    // printf("\n");
    return 0;
}

int LevelTraverse(StruQueList *queList, StruQueList **ptrQueListLastNode, char *target, StruHashTable **ptrDead, StruHashTable **ptrVisit)
{
    // Get the pointers of two queues, one is the current point, and the other is the new beginning corresponding to the level
    // Iterate new possibilities from each node of all layers
    int i, ret;
    while (queList != NULL) {
        // Traverse all nodes of the current layer
        int len = g_curLevelCnt;
        g_curLevelCnt = 0;
        for (i = 0; i < len; i++) {
            ret = NodeExpand(queList, ptrQueListLastNode, target, ptrDead, ptrVisit);
            if (ret > 0) { // Termination conditions
                return ret;
            }
            queList = queList->next;
        }
        // printf("\n\n");
    }

    return 0;
}

// According to the method of queue and BFS, it is expressed only once at a time, and all the corresponding possibilities
int openLock(char ** deadends, int deadendsSize, char * target)
{
    char cur[STR_SIZE] = "0000"; // Initial value
    int ret;

    // special case
    if (strcmp(cur, target) == 0) {
        return 0;
    }

    // Initialize dead hash
    StruHashTable *dead = NULL; // The header is empty at the beginning
    StruHashTable *hashTmp, *hashTmp1, *hashTmp2;
    InitDeadHash(deadends, deadendsSize, &dead);

    // special case
    HASH_FIND(hh, dead, target, sizeof(char) * STR_SIZE, hashTmp1);
    HASH_FIND(hh, dead, cur, sizeof(char) * STR_SIZE, hashTmp2);
    if (hashTmp1 != NULL || hashTmp2 != NULL) {
        return -1;  // deanends contain target
    }

    // Initialize queue 0000 and visit hash
    StruHashTable *visit = NULL; // The header is empty at the beginning
    PtrStruQueList queList, queListLastNode;
    InitQueAndVisitHash(cur, &queList, &visit);

    queListLastNode = queList;
    ret = LevelTraverse(queList, &queListLastNode, target, &dead, &visit);
    if (ret > 0) { // Termination conditions
        return ret;
    }
    return -1; // Traverse all no matches
}

expectation

  • Refer to the official solution to simplify the implementation of loop code in openLock()
  • Bidirectional BFS can be used to further improve efficiency
  • Heuristic search A * algorithm can be used to further improve efficiency

Topics: Algorithm data structure leetcode