【LeetCode回溯演算法#06】復原IP地址詳解(練習如何處理邊界條件,判斷IP合法性)

dayceng發表於2023-03-10

復原IP地址

力扣題目連結(opens new window)

給定一個只包含數字的字串,復原它並返回所有可能的 IP 地址格式。

有效的 IP 地址 正好由四個整數(每個整數位於 0 到 255 之間組成,且不能含有前導 0),整數之間用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 無效的 IP 地址。

示例 1:

  • 輸入:s = "25525511135"
  • 輸出:["255.255.11.135","255.255.111.35"]

示例 2:

  • 輸入:s = "0000"
  • 輸出:["0.0.0.0"]

示例 3:

  • 輸入:s = "1111"
  • 輸出:["1.1.1.1"]

示例 4:

  • 輸入:s = "010010"
  • 輸出:["0.10.0.10","0.100.1.0"]

示例 5:

  • 輸入:s = "101023"
  • 輸出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

  • 0 <= s.length <= 3000
  • s 僅由數字組成

思路

實際上還是分割問題,但是又多了一些條件

本題的麻煩的點在於有很多邊界條件需要處理

待解決的問題有:

1、如何加入分割點

2、如何判斷分割的段落是否為合法IP

插入分割點

"分割"這個動作與 分割回文串 裡定義的一致,都是用beginIndex指到分割位置

不同的是,這裡我們beginIndex指到分割的位置時,我們需要在這個位置的後一位插入分割點

(後面細說)

合法IP判斷

這裡我們需要寫一個判斷函式來判斷IP 是否合法,函式的輸入是字串待判斷的區間

bool isVaild(string& s, int start, int end){
    
}

這裡本質上我們還是在s上操作,只是指定了操作的位置

首先需要明確非法IP的情況:

​ 0、所給的判斷區間不合法

​ 1、以0開頭的數字,例如:192 . 031 . 2 . 1

​ 2、負數

​ 3、IP段數值大於255

bool isVaild(string& s, int start, int end){
    if(start > end) return false;//所給的判斷區間不合法
    
    //是否以0開頭
    if(s[start] == '0' && start != end) return false;
    
    int num4IP = 0;
    for(int i = start; i < end; ++i){
        //是否為正數
        if(s[i] < 0 && s[i] > 9) return false;
        
        //IP段數值是否大於255
        num4IP = num4IP * 10 + (s[i] - '0');
        if(num4IP > 255) return false;        
    }
    return true;
}

這裡有幾個關鍵點

1、判斷是否有IP段以0開頭

請注意,我們這裡判斷的是false的情況,也就是有IP段使用了0開頭

判斷條件除了直接看開頭字元是否為0(s[start] == '0')以外,還需要確保當前IP段的數字不是第一個獲取到的,因此需要追加條件 start != end

什麼意思呢?這裡又涉及到單層處理中的邏輯

在單層處理時,我們會去遍歷由beginIndex控制的s中的區間,每次遍歷都會觸發isVaild函式來判斷當前遍歷所得的IP段是否合法

isVaild函式需要輸入一個判斷區間(這個區間其實就是[beginIndex, i]

第一次遍歷的時候,輸入isVaild的區間是[0, 0],此時會出現一種特殊情況

例如,我們的數字字串是:s = "0000"

那麼第一次遍歷獲取IP段時,我們得到的是'0',從我們的角度來看,這種情況應該是合法的

如果我們不追加條件去判斷當前遍歷是不是第一次遍歷,那麼上述情況會被程式視為非法,就會導致錯誤

為什麼start != end可以判斷遍歷是否為第一次遍歷?

前面也說過了,因為我們輸入的區間實際上是[beginIndex, i],那麼這個區間只有在第一次遍歷時才會出現左右邊界相等的情況,即[0, 0]

2、判斷IP段數值大於255

這裡容易犯的一個錯誤是直接拿s[i]來和255進行大小判斷

拜託,s[i]是什麼?它是組成當前IP段的數字之一,單個數字怎麼可能大於255,這樣判斷只會透過所有的情況

因此,這裡需要一點小小的計算,我來模擬一下這個過程

	int num4IP = 0;
    for(int i = start; i < end; ++i){
        //是否為正數
        if(s[i] < 0 && s[i] > 9) return false;
        
        //IP段數值是否大於255
        num4IP = num4IP * 10 + (s[i] - '0');
        if(num4IP > 255) return false;        
    }

例如當前的IP段是164,

第一輪遍歷拿到s[i]是1,此時num = 0 * 10 + ('2' - '0') = 1

第二輪遍歷拿到s[i]是6,此時num = 1 * 10 + ('5' - '0') = 16

第三輪遍歷拿到s[i]是4,此時num = 16 * 10 + ('4' - '0') = 164

164顯然是滿足條件的

發現沒有, num = num * 10 + (s[i] - '0');的作用就是把資料型別為字串的IP段轉化為整型,然後再判斷

程式碼分析

開始寫,還是老一套,回溯三部曲

1、確定回溯函式的引數和返回值

本題中我們是直接操作的數字字串s,因此不需要返回值

輸入引數是數字字串s、beginIndex和countPoint

countPoint用於統計目前一共插入了幾個分割點,用於終止判定

class Solution {
private:
    bool isVaild(string& s, int start, int end){
        ...
    }
    vector<string> res;
    //確定回溯函式的引數和返回值
    //引數:數字字串、beginIndex、分割點計數變數
    void backtracking(string& s, int beginIndex, int countPoint){        
    }
public:
    vector<string> restoreIpAddresses(string s) {        
    }
};

2、確定終止條件

這裡就用到countPoint了

參考IP的結構,當我們已經插入了3個分割點時,這時需要結束了(不論之後還剩什麼)

同時,我們還要判斷一下被分割出來的第四段是否合法,合法就把當前插好的數字字串s儲存到結果陣列(一定記得我們是對s進行操作,最後的結果也是處理好的s)

class Solution {
private:
    bool isVaild(string& s, int start, int end){
        ...
    }
    vector<string> res;
    //確定回溯函式的引數和返回值
    //引數:數字字串、beginIndex、分割點計數變數
    void backtracking(string& s, int beginIndex, int countPoint){
        if(countPoint == 3){
            if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
                res.push_back(s);                
            }
            return;
        }
    }
public:
    vector<string> restoreIpAddresses(string s) {        
    }
};

3、確定單層處理邏輯

在單層遞迴中,我們需要遍歷由beginIndex控制的當前區間

使用for迴圈不斷獲取IP段,然後判斷其是否合法

合法就在當前位置 i 的後一個位置插入分割點

class Solution {
private:
    bool isVaild(string& s, int start, int end){
        ...
    }
    vector<string> res;
    //確定回溯函式的引數和返回值
    //引數:數字字串、beginIndex、分割點計數變數
    void backtracking(string& s, int beginIndex, int countPoint){
        //確定終止條件
        //當分割點的數量達到3個時說明分割結束,此時字串已經被分成4段
        //在這裡還需要驗證最後一段是否合法
        if(countPoint == 3){
            if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
                res.push_back(s);                
            }
            return;
        }
        //確定單層處理邏輯
        for(int i = beginIndex; i < s.size(); ++i){
            //這裡beginIndex不是固定的,下一層遞迴時beginIndex會由上一層遞迴傳入
            if(isVaild(s, beginIndex, i)){                
                s.insert(s.begin() + i + 1, '.');//插入分割點
                countPoint++;//計數++
                backtracking(s, i + 2, countPoint);//只加1的話就到分割點的位置,所以得多加一個
                countPoint--;//回溯
                s.erase(s.begin() + i + 1);
            }
        }
    }
public:
    vector<string> restoreIpAddresses(string s) {        
    }
};

這裡又涉及到一個邊緣處理問題:插入分割點的位置

題外話:insert函式

本題中使用insert來在s中插入分割點

舉個例子說明insert的使用:

teststr = "1234";
teststr.insert(1, '.');//在原串下標為1的字元1前插入'.'->"1.234"

所以為什麼插分割點的時候的位置是s.begin() + i + 1

因為s.begin() + i只定位到了當前指標 i 遍歷到的位置,而我們希望在該位置之後加點,所以要再加1

例如遍歷"1234",遍歷時 i 指向2時,即下標2,我們需要在2後面打點,此時要輸入insert的應該是下標3而不是下標2

完整程式碼

本題思路很清晰,但是難點在於有很多的邊界條件

一旦處理不好或者忘了,就會出錯

class Solution {
private:
    bool isVaild(string& s, int start, int end){
        //判斷區間是否合法
        if(start > end) return false;
        //判斷數字開頭是否為0,為0非法
        if(s[start] == '0' && start != end){
            return false;
        }
        int num4IP = 0;
        //遍歷待判斷區間
        for(int i = start; i <= end; ++i){
            //是否為正數
            if(s[i] > '9' || s[i] < '0'){
                return false;
            }
            //是否在255範圍內
            num4IP = num4IP * 10 + (s[i] - '0');
            if(num4IP > 255){
                return false;
            }
        }
        return true;
    }
    vector<string> res;
    //確定回溯函式的引數和返回值
    //引數:數字字串、beginIndex、分割點計數變數
    void backtracking(string& s, int beginIndex, int countPoint){
        //確定終止條件
        //當分割點的數量達到3個時說明分割結束,此時字串已經被分成4段
        //在這裡還需要驗證最後一段是否合法
        if(countPoint == 3){
            if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
                res.push_back(s);                
            }
            return;
        }
        //確定單層處理邏輯
        for(int i = beginIndex; i < s.size(); ++i){
            //這裡beginIndex不是固定的,下一層遞迴時beginIndex會由上一層遞迴傳入
            if(isVaild(s, beginIndex, i)){                
                s.insert(s.begin() + i + 1, '.');//插入分割點
                countPoint++;//計數++
                backtracking(s, i + 2, countPoint);//只加1的話就到分割點的位置,所以得多加一個
                countPoint--;//回溯
                s.erase(s.begin() + i + 1);
            }
        }
    }
public:
    vector<string> restoreIpAddresses(string s) {
        //這裡可以先判斷一下字串s是否合法
        if(s.size() < 4 || s.size() > 12) return res;
        backtracking(s, 0, 0);
        return res;
    }
};

相關文章