基础算法整理(十二)

转载请注明出处:https://hts0000.github.io/

欢迎与我联系:hts_0000@sina.com

数位统计DP

数位DP强调分情况讨论

计数问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
package main

import (
    "bufio"
    "fmt"
    "os"
)

var (
    in = bufio.NewReader(os.Stdin)
    out = bufio.NewWriter(os.Stdout)
)

func power10(x int) int {
    ans := 1
    for x > 0 {
        ans *= 10
        x--
    }
    return ans
}

// 返回 1~n 中 x 出现次数
func count(n, x int) int {
    ans := 0
    cnt := 0
    
    // 统计 n 有多少位
    for m := n; m > 0; m /= 10 { cnt++ }
    
    // 从右往左枚举每一位上的 x 总数
    for i := 1; i <= cnt; i++ {
        // 我们计算第四位上 x 出现的次数
        // 假设 n = abcdefg,i = 4 指向 d 这一位
        
        // 情况1:高三位为 000~abc-1
        // d 右边可以取到 000~999 共 power10(i - 1) 个数
        r := power10(i - 1)
        // d 左边可以取到 000 ~abc-1 共 abc 种情况
        l := n / (r * 10)
        // abc = n / power10(i) = n / (r * 10)
        // 当 x == 0 时,则为 001~abc-1 种情况
        if x > 0 {
            ans += l * r
        } else {
            ans += (l - 1) * r
        }
        // n / r = abcd, abcd % 10 = d
        d := (n / r) % 10
        // 高三位等于 abc 的情况,只需要考虑 d >= x,因为 d < x 就不符合条件
        // 前四位 abcd 均相同,后三位可取 0~efg 共 efg + 1 种情况
        if d == x {
            ans += n % r + 1
        // 此时后三位可取 000~999 共 power10(i - 1) 种情况
        } else if d > x {
            ans += r
        }
    }
    return ans
}

func main() {
    defer out.Flush()
    
    for {
        var a, b int
        
        fmt.Fscan(in, &a, &b)
        if a == 0 && b == 0 { break }
        
        if a > b { a, b = b, a }
        
        for i := 0; i < 10; i++ {
            fmt.Fprintf(out, "%d ", count(b, i) - count(a - 1, i))
        }
        fmt.Fprintln(out)
    }
}

经典模板题

338. 计数问题
233. 数字 1 的个数

状态压缩DP

状态压缩就是把一个集合的状态,压缩成一个数,用这个数的二进制表示集合的每一种情况。再使用位运算、逻辑运算等操作方便的计算状态。

蒙德里安的梦想

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package main

import (
    "bufio"
    "fmt"
    "os"
)

const (
    N = 12 // 以便计算最后一列 d[m][0]
    M = 1<<N
)

var reader = bufio.NewReader(os.Stdin)
var writer = bufio.NewWriter(os.Stdout)

var st [M]bool // 字典表,存储所有位置的合法情况

func initStatus(n int){
    for i:=0;i<1<<n;i++{
        cnt:=0
        st[i] = true
        for j:=0;j<n;j++{
            // 遇到1,被占, 且统计的空格数为奇数,无法填充竖着的长方形
            if i>>j & 1 > 0 {
                if cnt&1>0{
                    st[i] = false
                    break
                }
                cnt=0
                continue
            }
            cnt++
        }

        // 判断剩下的统计数是否奇数
        if st[i] && cnt&1>0{
            st[i] = false
        }
    }
}

// - **数位统计DP**: 所谓的状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩
//  - 计数问题: 蒙德里安的梦想, 求把N*M的棋盘分割成若干个1*2的的长方形,有多少种方案。
//      - 核心: 找到所有横放 1 X 2 小方格的方案数,因为所有横放确定了,那么竖放方案是唯一的。
//      - 状态定义:
//          - 集合: 用`f[i][j]` 记录第i列第j个状态的所有合法方案。j二进制中1表示对应行的上一列有横放格子并捅出到本列中。
//          - 属性: 方案数
//      - 状态计算: `f[i][j] += f[i - 1][k]`
//          - i 列和 i - 1 列同一行不同时捅出来 ; 本列捅出来的状态j和上列捅出来的状态k求或,得到上列是否存在连续奇数空行状态,奇数空行不转移。
//          - `f[m][0]` 表示没有m列没有涌出。
var f [N][M]int
func solution(n,m int) int{
    initStatus(n)

    f[0][0] = 1

    for i:=1;i<=m;i++{
        for j:=0;j<1<<n;j++{
            f[i][j] = 0  // 重置统计
            for k:=0;k<1<<n;k++{
                if j&k==0 && st[j|k] {
                    f[i][j] +=f[i-1][k]
                }
            }
        }

    }

    return f[m][0] // 最后一列
}

func main() {
    var n,m int
    for {
        fmt.Fscan(reader, &n, &m)
        if n | m == 0 {
            break
        }
        fmt.Fprintln(writer, solution(n,m))
    }
    writer.Flush()
}

91. 最短Hamilton路径

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import "fmt"

/*
举例说明:如果是4个点
0->1->2->3
0->2->1->3
...总方案是3!,剩下的4种方法略...
假设第一种走法的方案,距离是10,第二种走法是20,
则第二种走法后面的点再怎么走的方案也就不被采纳,并且后面点的方法是可以直接套在第一种方案后的。
所以我们就可以只关注两点:
1、那些点是走过的
2、现在走在了哪个点上
状态表示:
f[status][j] 第一维表示走过的所有点,第二维度表示现在落在哪个点上
状态转移:
f[status][j]=f[status_k-j][k]+weight[k][j]  k表示当前走到的j点的前一步k点,并且k点的所有状态集合status_k中不包含j点
status的状态使用二进制状态压缩,1,0分别表示走过和没走过
*/

const (
	N = 20
	M = 1 << 20
)

var (
	n      int
	f      [M][N]int
	weight [N][N]int
)

func main() {
	fmt.Scan(&n)
	// 输入
	for i := 0; i < n; i++ {
		for j := 0; j < n; j++ {
			fmt.Scan(&weight[i][j])
		}
	}
	// 初始化f数组
	for i := 0; i < M; i++ {
		for j := 0; j < N; j++ {
			f[i][j] = 0x3f3f3f3f
		}
	}

	f[1][0] = 0 // 在起点的状态,还没走,所以距离是0

	for i := 0; i < 1<<n; i++ {
		for j := 0; j < n; j++ {
			if i>>j&1 > 0 { // 查看集合i中的j点有没有走过,只有走过的点的集合才是可以状态转移的合法状态
				for k := 0; k < n; k++ { //枚举所有的整数
					if i-(1<<j)>>k&1 > 0 { //当前状态i集合还不包括j点,所以集合中要减去,减去后的集合也得满足合法的状态转移条件
						// 所以k点是已经走过的,二进制1表示。
						f[i][j] = min(f[i][j], f[i-(1<<j)][k]+weight[k][j])
					}
				}
			}
		}
	}
	fmt.Println(f[(1<<n)-1][n-1])
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

经典模板题

291. 蒙德里安的梦想
最短Hamilton路径

树形DP

没有上司的舞会

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
    "bufio"
    "fmt"
    "os"
)

const N int = 6010

var (
    in = bufio.NewReader(os.Stdin)
    out = bufio.NewWriter(os.Stdout)
    
    h, e, ne [N]int
    idx int
    
    happy [N]int
    
    f [N][2]int
    
    hasFather [N]bool
    
    n int
)

func add(a, b int) {
    e[idx] = b
    ne[idx] = h[a]
    h[a] = idx
    idx++
}

func dfs(u int) {
    f[u][1] = happy[u]
    
    for i := h[u]; i != -1; i = ne[i] {
        j := e[i]
        dfs(j)
        
        f[u][1] += f[j][0]
        f[u][0] += max(f[j][0], f[j][1])
    }
}

func main() {
    defer out.Flush()
    
    fmt.Fscan(in, &n)
    
    for i := 1; i <= n; i++ { fmt.Fscan(in, &happy[i]) }
    
    for i := 0; i < N; i++ { h[i] = -1 }
    
    for i := 0; i < n - 1; i++ {
        var a, b int
        fmt.Fscan(in, &a, &b)
        add(b, a)
        hasFather[a] = true
    }
    
    root := 1
    for hasFather[root] { root++ }
    
    dfs(root)
    
    fmt.Fprintln(out, max(f[root][0], f[root][1]))
}

func max(a, b int) int {
    if a > b { return a }
    return b
}

记忆化搜索

滑雪问题

滑雪

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package main

import (
    "bufio"
    "fmt"
    "os"
)

const N int = 310

var (
    in = bufio.NewReader(os.Stdin)
    out = bufio.NewWriter(os.Stdout)
    
    g [N][N]int
    f [N][N]int
    
    dx = [4]int{-1, 0, 1, 0}
    dy = [4]int{0, 1, 0, -1}
    
    n, m int
)

func dp(x, y int) int {
    if f[x][y] != -1 { return f[x][y] }
    
    f[x][y] = 1
    
    for i := 0; i < 4; i++ {
        a := x + dx[i]
        b := y + dy[i]
        if a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b] {
            f[x][y] = max(f[x][y], dp(a, b) + 1)
        }
    }
    
    return f[x][y]
}

func main() {
    defer out.Flush()
    
    fmt.Fscan(in, &n, &m)
    
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            fmt.Fscan(in, &g[i][j])
        }
    }
    
    for i := 0; i < N; i++ {
        for j := 0; j < N; j++ {
            f[i][j] = -1
        }
    }
    
    ans := 0
    for i := 1; i <= n; i++ {
        for j := 1; j <= m; j++ {
            ans = max(ans, dp(i, j))
        }
    }
    
    fmt.Fprintln(out, ans)
}

func max(a, b int) int {
    if a > b { return a }
    return b
}