1. 程式人生 > >【演算法】求字串子串的高效實現

【演算法】求字串子串的高效實現

出處

有一道秋招筆試題是這樣:

輸入兩個正整數n、d(1<=d<=1000),把n看成字串,求n的所有子串中能被d整除的子串的個數。

示例1:

輸入:1234 4,輸出:4,說明:12、124、24、4

示例2:

輸入:616 3,輸出:3,說明:6、66、6

分析

主要難點是求看成字串的n的所有子串,必須要快速高效地求出,除了回溯法,還有一種二進位制移位操作演算法如下:

組合

首先看一下組合的含義:

從m個不同元素中,任取出n個成一組,稱為一個組合。

注意:得是按照原順序取,例如在124中,24是組合,但42不是(是排列)。

組合總數:

從n個元素中,取0個元素的組合數+取1個元素的組合數+……+取n個元素的組合數=2^n

移位

移位操作如下:

假如要求124的組合數,那麼有2^3=8種組合,用二進位制位表示如下:

000、001、010、011、100、101、110、111,

就是將0-7用二進位制位表示而已,注意這裡二進位制位有0有1,我們可以規定:0表示不取,1表示取。

如何判斷某一位上是0還是1?

重點來了,通過移位操作,這裡用Java實現,int型別的長度為32,現舉例如下:

(1)011,判斷第0個位是1。

這裡的第0位意思是2的0次方。

首先將011左移位31位,這步的作用是將第0位的1移動到int的首位31位,即:

10000000 00000000 00000000 00000000,

然後再右移位31位,就將首位移動到了末位,即:

11111111 11111111 11111111 11111111,

注意這裡是有符號右移>>,不是無符號>>>,所以如果首位是0,那麼右移位之後是0,如果首位是1,那麼右移位後全是1。那麼這個二進位制數是十進位制的幾呢?是-1,這裡用到了補碼的知識。

補碼

對於負數的補碼,求十進位制的補碼是:取絕對值的二進位制表示,取反,加一,然後首位為1即可。舉例如下:

(1)求-1的補碼。

-1的絕對值是1,即0000000 00000000 00000000 00000001,注意這裡只有31位,暫不考慮首位。

取反:1111111 11111111 11111111 11111110,

加一:1111111 11111111 11111111 11111111,

首位為1:11111111 11111111 11111111 11111111。

所以32個1的二進位制位表示十進位制的-1。再看一個示例加強記憶。

(2)求-128的補碼,這裡為了方便,假設數位長度為8,即Java中的byte型別。

-128的絕對值為128,即1000 0000,不考慮首位,那就是000 0000。

取反:111 1111,

加一:000 0000,這裡溢位了,不管。

首位為1:1000 0000。

所以-128的補碼為1000 0000,這按原碼來算,正是128,所以有點巧妙。

言歸正傳,下面看第2個例子。

(2)011,判斷第1個位是1。

因為是第1位,所以要左移位的位數就比末位少了1位,即左移31-1=30位,左移位後為:

11000000 00000000 00000000 00000000,

可以看到,末位的1也保留了,但不要緊,在右移位時會消除掉它。

右移位31位,這個位數是固定的,目的是將首位移至末位。右移位後為:

11111111 11111111 11111111 11111111,

不要忘記這裡是有符號右移位,所以還是全1,那麼是十進位制的幾呢?是-1。

第3個例子應該就簡單了,如下:

(3)011,判斷第2個位是0。

因為是第2位,所以要左移位的位數就比末位少了2位,即31-2=29位,左移位後為:

01100000 00000000 00000000 00000000,

可以看到,後面的位都保留了,但也不要緊,在右移位時會消除掉它們。

右移位31位,這個位數還是固定的,目的是將首位移至末位。右移位後為:

00000000 00000000 00000000 00000000,

這個毫無疑問是0。

再回到要解決的問題上來,目標是求1234的全組合,那麼它的組合總數是多少呢?是2^4=16,可以通過將1左移位來求得,即1<<4,4是1234的長度。

然後這些組合分別是什麼呢?是0-15的二進位制位,即0000、0001、……、1111,那麼如何得到每個位上是0還是1呢?當然是左移位和右移位啦。計算機運算的原理是二進位制的位操作,所以移位的效率還是蠻高的。

解決

那麼問題解決的演算法就出來了:

(1)判斷字串的長度n,得到組合總數1<<n,此為外迴圈。

(2)每個組合數的二進位制長度n,此為內迴圈,對每一位都左移右移判斷是0還是1。

(3)若是1,則取之,若是0,則舍之。這裡可以用StringBuilder來暫存字元。

(4)具體問題,判斷是否整除,那麼取餘操作%就好。

程式碼如下:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(new Main().getClass().getResourceAsStream("Main"));
        while (in.hasNext()) {
            long n = in.nextLong();
            int d = in.nextInt();
            char str[] = String.valueOf(n).toCharArray();
            int dd = String.valueOf(d).length();
            int nCnt = str.length;
            int nBit = 1 << nCnt;
            int count = 0;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < nBit; i++) {
                // 每個組合都要先清空StringBuilder
                sb.delete(0, sb.length());
                for (int j = 0; j < nCnt; j++) {
                    // -1意為第j位是1,否則是0
                    if ((i << (31 - j)) >> 31 == -1) {
                        sb.append(str[j]);
                    }
                }
                String s = sb.toString();
                if (s.length() >= dd && Integer.valueOf(s) % d == 0) {
                    count++;
                    System.out.print(s + " ");
                }
            }
            System.out.println(count);
        }
    }
}

程式碼說明:

Scanner中用到了類所在目錄中檔案Main中的測試資料:

1234 4
616 3
1234567890 23

執行結果如下:

12 4 24 124 4
6 6 66 3
23 345 46 2346 1357 138 368 124568 13478 45678 2345678 69 2369 34569 123579 1679 24679 12489 14789 1256789 230 3450 460 23460 13570 1380 3680 1245680 134780 456780 23456780 690 23690 345690 1235790 16790 246790 124890 147890 12567890 40

這個演算法實測是AC的。

以上就是我做這道題的感悟,如有疏忽,請大家不吝賜教。

參考: