算法实验报告2

本文链接:https://type.dayiyi.top/index.php/archives/231/

1.求幂集问题

也就是求全部的组合

DFS:

  • 把全排列DFS树给记录下来就可以
  • DFS到每个节点的时候,记录当前状态加入到结果集即可。

复杂度O(N!)

python代码:

def dfs(nums, path, index, res):
  res.append(path[:])
  for i in range(index, len(nums)):
    path.append(nums[i])
    dfs(nums, path, i+1, res)
    # 回溯
    path.pop()

N = 5
nums = [i for i in range(1,N+1)]
res = []
dfs(nums, [], 0, res)
print(res)

位运算

位运算

复杂度O(N*2^n)

  • 假设有5个物品,每个物品可以选或者不选。
  • 0 0 0 0 0 5个二进制,1表示选,0表示不选
  • 这样下来,2^5次方就可以把全部的情况枚举出来。
  • 整数0的二进制表示00000对应空集。
  • 整数1的二进制表示00001对应只含有第一个元素的子集。
  • 整数2的二进制表示00010对应只含有第二个元素的子集。
  • ...以此类推...
  • 整数31的二进制表示11111对应包含所有五个元素的集合本身。
  • 这样下来,每个数都是一个子集,求完即可。
  • 每一个从\(0\)\(2^n - 1\)的整数都对应一个唯一的子集。对于每个整数,检查其二进制表示中的每一位,如果当前位是1,表示选中,当前位是0,便不选。

具体的:

  • 位操作符&
  • i是当前数
  • (1 << j) 把1左移j
  • i = 01000
  • j = 1<<4 = 0100
  • i & j = 1

如果 i & j = 1 则说明当前位是1,选中当前的元素。

N = 15
nums = [i for i in range(1,N+1)]
res = []
def bits(nums):
  # N的所有子集的个数为 2^n
  n = len(nums)
  for i in range(2**n):
    subset = []
		# 检测每一位
    for j in range(n):
      # i的2进制的j位是1,把1左移j位,然后跟i进行AND运算,如果运算结果是1,则说明i的2进制当前为是1
      if i & (1 << j):
        subset.append(nums[j])
    res.append(subset)
bits(nums)
print(res)

2.N皇后问题 实现回溯法求解皇后问题递归和非递归框架

N皇后,在N×N的棋盘上放置N个皇后,使得它们不能相互攻击,即任何两个皇后都不能处在同一行、同一列或同一对角线上。

  • 枚举每个点
  • 检查当前的点是否可以放置皇后

检查函数

  • 斜行

具体的:

因为一个行 一个列 一个斜行 只能放置一个皇后

直接标记当前行列是否可以放置皇后。

  • 对于每一行 一个数组 row[N]
  • 对于每一列 一个数组 col[N]
  • 对于每一斜行
    • 对角线
    • 反对角线

对于对角线来说:

  • 每条直线可以表示为:\(y = -x+b\)
  • 截距:\(b = y+x\)
  • 每个截距可以表示一条对角线
  • 根据取值,于是,对于每个单元\((x,y)\)的对角线,就可以 dg[y+x]来进行表示。

而对于反对角线来说:

同样的:

  • 每条直线可以表示为:\(y = x+b\)
  • 截距:\(b = y-x\)
  • 每个截距可以表示一条对角线
  • 根据取值,于是,对于每个单元\((x,y)\)的对角线,就可以 adg[y-x]来进行表示。

但是根据定义域来说,\(x+y\)可能为负数,对于计算机来说,可能会导致数组越界。而我们只需要表示当前行是否被占,直接对于每个数\(+N\)

也就是用adg[y-x+N]来表示\((x,y)\)的对角线有没有被占。

综上:

  • \(row[x]\) (如果DFS按照这个顺序枚举,其实不需要添加)
  • \(col[y]\)
  • 对角线 \(adg[y+x]\)
  • 反对角线\(adg[y-x+N]\)

检查函数可以这样写:

row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

对于放置函数
其实就是标注一下,但是这样可以提升写代码的幸福感。

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0

DFS 搜

时间复杂度\(O(n!)\)

简单写一下:

# 检查当前位置是否可以放置
n = 8 

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    return 1
  for i in range(1,n+1):
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res

n = 8
print(dfs(1))

带结果打印:

# 检查当前位置是否可以放置
n = 8 
n = 5

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

# 打印
board = [['.' for _ in range(n)] for _ in range(n)]

def print_res():
  for i in range(n):
        print(' '.join(board[i]))
  print('\n')

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1
  # 打印结果用
  board[x-1][y-1] = 'Q'

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0
  # 打印结果用
  board[x-1][y-1] = '.'


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    print_res()
    return 1
  
  for i in range(1,n+1):
    # 把 xy标记一下
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res
print(dfs(1))

# 检查当前位置是否可以放置
n = 8 
n = eval(input())

# +10 防止数组越界
row = [0 for i in range(n+10)]
col = [0 for i in range(n+10)]
dg = [0 for i in range(n*n+10)]
adg = [0 for i in range(n*n+n+10)]

# 打印
board = [['.' for _ in range(n)] for _ in range(n)]

res_cnt = 0

def print_res():
  global res_cnt
  if res_cnt==3:
    return
  for i in range(1,n+1):
    for idx,j in enumerate(board[i-1]):
      if(j=='Q'):
        print(idx+1,end=" ")
      # print(idx,j)
  print("")
  res_cnt+=1
    # if board[i] =='Q':
    #   print(i,end=" ")
    
  
  return
  # for i in range(n):
  #       print(' '.join(board[i]))
  # print('\n')

def check(x,y):
  if row[x] == 1:
    return 0
  if col[y] == 1:
    return 0
  if dg[x+y]==1:
    return 0
  if adg[y-x+n]==1:
    return 0
  return 1

def place(x,y):
  row[x]=1
  col[y]=1
  dg[x+y]=1
  adg[y-x+n]=1
  # 打印结果用
  board[x-1][y-1] = 'Q'

def unplace(x,y):
  row[x]=0
  col[y]=0
  dg[x+y]=0
  adg[y-x+n]=0
  # 打印结果用
  board[x-1][y-1] = '.'


ans = 0
def dfs(t):
  res = 0
  if t==n+1:
    print_res()
    return 1
  for i in range(1,n+1):
    # 把 xy标记一下
    x = t
    y = i
    if(check(x,y)):
      place(x,y)
      res += dfs(t+1)
      unplace(x,y)
  return res


print(dfs(1))

TLE了,但是我觉得python已经很快了。

非递归

用栈模拟的,我实在没想到怎么用

# 初始化棋盘参数
n = 8  # 皇后数量和棋盘大小

# +10 是为了防止数组越界
row = [0 for _ in range(n+10)]
col = [0 for _ in range(n+10)]
dg = [0 for _ in range(n*n+10)]
adg = [0 for _ in range(n*n+n+10)]

# 打印棋盘
def print_board(board):
    for i in range(n):
        print(' '.join(board[i]))
    print('\n')

# 检查位置是否可以放置皇后
def check(x, y):
    if row[x] == 1 or col[y] == 1 or dg[x+y] == 1 or adg[y-x+n] == 1:
        return False
    return True

# 放置皇后
def place(x, y):
    row[x] = 1
    col[y] = 1
    dg[x+y] = 1
    adg[y-x+n] = 1

# 移除皇后
def unplace(x, y):
    row[x] = 0
    col[y] = 0
    dg[x+y] = 0
    adg[y-x+n] = 0

# 非递归解决N皇后问题
def solve_stack(n):
    board = [['.' for _ in range(n)] for _ in range(n)]  # 初始化棋盘
    ans = 0  # 解的数量
    stack = [(0, list(range(n)))]  # 使用栈进行深度优先搜索,包含行索引和列候选
    place_pos = []  # 用于回溯的放置皇后位置
    while stack:
        row, col = stack[-1]
        if not col:  # 如果当前行没有列可尝试,进行回溯
            stack.pop()
            if place_pos:
                last_row, last_col = place_pos.pop()
                board[last_row][last_col] = '.'
                unplace(last_row + 1, last_col + 1)
            continue

        col = col.pop()
        if check(row + 1, col + 1):
            place(row + 1, col + 1)
            board[row][col] = 'Q'
            place_pos.append((row, col))
            if row == n - 1:  # 找到一个解
                ans += 1
                print_board(board)
                board[row][col] = '.'
                unplace(row + 1, col + 1)
                place_pos.pop()
            else:
                stack.append((row + 1, list(range(n))))  # 移动到下一行
        # 继续尝试当前行的下一个列
    return ans
print(solve_stack(n))

3. 01包问题

这个,讲真如果第一次学DP的话,肯定会有点难理解。

问题:

\(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。

\(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

爆搜,对于每个物品进行枚举一次

这个方案如T1的枚举,时间复杂度是\(O(N!)\)或者\(O(2^N*N)\)

\(N\)大于20的时候,肯定是搜不动了。

因此当N = 1000的时候,该方案应该是不可取的。

DP

状态属性:

  • 状态表示:f[i][j]
  • 集合:装了前i个物品,总体积不超过j的选法的集合
  • 属性:Max值

状态转移

对于第 \(i\) 个物品,可以选择装或者不装。

  • 我不打算装这个物品

    我的体积不需要被消耗
    我的最大的值没有变,可以直接f[i-1][j]的状态转移过来。
    \(f[i][j] = f[i-1][j]\)

  • 我打算装这个物品:

    • 我需要w[i]的空间
    • 我能获得的价值是v[i]
    • 当前我一定要装这个物品,一定要花费w[i]的空间(重量)
    • 我装完之后的背包重量不能大于j
      于是我的状态转移方程:
      \(f[i][j] = f[i-1][j-w[i]]+v[i]\)
  • 两种情况都可能会影响到后续的结果,因此,需要将两个值合并为一个状态(\(f[i][j]\)

    • \(f[i][j]\)的属性是MAX,直到最后,我只要出现过的最大值,如果当前的值小于\(f[i-1][j]\)的值
      • 也就是如果
      • \(f[i-1][j]\) =10
      • \(f[i-1][j-w[i]]+v[i]\) = 9 (假设)
      • \(f[i][j]\) 应该等于10

最后的状态转移方程为:

\(f[i][j] = MAX\{f[i-1][j],f[i-1][j-w[i]]+v[i]\}\)

(截个图防止公式坏掉)

对于最终的结果显然就是:

\(dp[N][W]\) 这个值是前N个物品,重量小于等于W的背包可以获得的最大值

对于处理的过程,要注意:

  • \(j-w[i]\)应该大于0,背包空间如果小于0肯定不太合理,也会数组越界

然后直接去写代码就好啦

#include<iostream>
const int maxn = 1e4+102;
int dp[maxn][maxn];
int w[maxn],v[maxn];
int N,W;
int main(){
  using namespace std;
  cin>>N>>W;
  for(int i=1;i<=N;++i){
    cin>>w[i]>>v[i];
  }

  //dp 不装东西的时候假设价值是0
  for(int i=1;i<=N;++i){
    dp[i][0]=0;
  }
  
  for(int i=1;i<=N;++i){
    for(int j=0;j<=W;++j){
      if(j-w[i]<0)dp[i][j]=dp[i-1][j];
      else dp[i][j] = max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
    }
  }
  cout<<dp[N][W];
}

通过测试:

压缩下数组:

  • 你会发现,我们的状态dp[i][j]只会从:
    • dp[i-1][j]
    • dp[i-1][j-w[i]]
  • 这两个状态转移过来
  • 也就是说之前的状态与最终答案和当前计算的状态没有关系
  • \(dp[i][j]\) 只依赖于 \(dp[i-1][j]\)\(dp[i-1][j-w[i]]\)

动态规划状态压缩的核心在于识别出状态转移仅依赖于有限的几个状态。

但是注意:

  1. 我们只关心 "当前" 和 "上一个" 两个状态,这样就可以只用一个一维数组啦。
  2. 但是为了确保 "当前状态" 是基于 "上一个状态" 计算的,需要须反向遍历背包容量(重量)。
  3. 因为如果正向遍历,当计算到 \(dp[j]\) 时,\(dp[j-w[i]]\) 可能已经在这一轮循环中更新过了,使用这一轮的信息而不是上一轮的信息。

压缩后的状态转移方程为:

\[dp[j] = \max\{dp[j], dp[j-w[i]] + v[i]\} \]

  • dp[j] 表示在不超过重量 j 的前提下,目前为止能得到的最大价值。
  • dp[j-w[i]] + v[i] 表示如果你选择放入第 i 个物品,那么背包中剩余的重量就是 j-w[i],对应的最大价值就是 dp[j-w[i]],加上第 i 个物品的价值 v[i] = dp[j-w[i]] + v[i]

对于状态转移:

  • 不放第 i 个物品时,背包的最大价值(即 dp[j])。
  • 放入第 i 个物品时,背包的最大价值(即 dp[j-w[i]] + v[i])。

然后取这两种情况的\(MAX\)作为新的 dp[j] 的值。

最后:dp[W] 就是小于W能获得的最大价值。

每次计算 dp[j] 时,dp[j-w[i]] 保持的是上一个物品状态的值。

dp[j-w[i]] 是基于第 i-1 个物品时的最大价值

如果正序遍历,那么在计算较大的 j 值时,可能会重复计算某个物品的价值。

OK

这样代码如下:

#include<iostream>
const int maxn = 1e4+102;
int dp[maxn]; 
int w[maxn],v[maxn];
int N,W;
int main(){
  using namespace std;
  cin>>N>>W;
  for(int i=1;i<=N;++i){
    cin>>w[i]>>v[i];
  }
  //清零(可以不用)
  for(int i=0;i<=W;++i)dp[i]=0;
  for(int i=1;i<=N;++i){
    for(int j=W;j>=0;--j){ //逆序遍历
      if(j-w[i]>=0){
        // 当 j<w[i] 时,dp[j] = dp[j]
        dp[j]=max(dp[j], dp[j-w[i]] + v[i]);
      }
    }
  }
  cout<<dp[W]; //dp[W]就是结果啦
}

空间复杂度从 \(O(NW)\) 降低到了 \(O(W)\),其实这里读入的数组的时候可以直接算dp方程,还能再省,但是一般空间不会不够用。

然后写个python的版本

maxn = 1000+101
dp = [0 for i in range(1,maxn)]
w = [0 for i in range(1,maxn)]
v = [0 for i in range(1,maxn)]

N,W = map(int,input().split())

for i in range(1,N+1):
  w[i],v[i] = map(int,input().split())
  for j in range(W,0,-1):
    # print(j)
    if j-w[i]>=0:
      dp[j]=max(dp[j],dp[j-w[i]]+v[i])
print(dp[W])

4.数独问题

你需要把一个 9×9的数独补充完整,使得数独中每行、每列、每个 3×3的九宫格内数字 1∼9均恰好出现一次。

可以直接拿一道题过来:

https://vjudge.net/problem/POJ-3074#author=GPT_zh
https://www.acwing.com/problem/content/description/168/

这里的输入:

.2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end

其实还是跟八皇后类似

  • 检查函数
  • 枚举状态
  • 剪枝(新增)

对于检查函数:

  • 检查当前行是否合法
  • 检查当前列是否合法
  • 斜行不需要检测,但是需要检查当前九宫格内
# 暴力检查,没有优化
def check(x, y, val):
    # 检查行
    for i in range(9):
        if mp[x][i] == val:
            return False
    # 检查列
    for i in range(9):
        if mp[i][y] == val:
            return False
    # 检查3x3的格子
    startRow = x - x % 3
    startCol = y - y % 3
    for i in range(3):
        for j in range(3):
            if mp[startRow + i][startCol + j] == val:
                return False
    return True

然后是DFS

# 拆开
mp =[inp[i:i+9] for i in range(0, 81, 9)]
mp = [[int(char) for char in row] for row in mp]
def dfs(mp):
  # 寻找空的单元格
  for x in range(9):
    for y in range(9):
      if mp[x][y] == 0:
        # 尝试所有可能的数字
        for val in range(1, 10):
          if check(x, y, val,mp):
            mp[x][y] = val
            if dfs(mp):
              return True
            # 回溯
            mp[x][y] = 0
        return False
  return True
if dfs(mp):
    ans = mp
else:
    ans = "No solution exists"

print(ans)

结果如图:

结果也正确:

试试提交:

很遗憾,Vjudge不支持Python

在acwing上,虽然结果可以正确,但是样例都会TLE

剪枝:

  • 选择下一个要填充的单元格时,优先选择候选数字最少的单元格。
  • check函数可以优化

没优化明白,明天用c++再写一遍。