利用强化学习训练“井字棋”下棋策略的详细介绍

news2025/9/21 18:05:08

文章目录

  • 1. 引言
  • 2. 基本要素
    • 2.1 棋盘状态
    • 2.2 智能体(棋手)
    • 2.3 用户交互接口
  • 3. 训练过程
    • 3.1 模拟对弈过程
    • 3.2 策略的训练及检验
    • 3.3 用户实践


本篇文章的强化学习例子的完整代码来自刘建平老师的Github仓库。

1. 引言

井字棋游戏(Tic-Tac-Toe)是强化学习入门常用的一个简单案例,它的规则是,在一个 3 × 3 3\times 3 3×3 的九宫格里,两个人轮流下棋,直到有个人的棋子能连成长度为 3 3 3 的线(一行一列或者一对角线)则赢得比赛,如果九宫格填满后也没人胜出,则和棋。

这里通过强化学习训练一个下棋机器人,由于棋盘规模小,对局状态有限且能完全遍历,因此由人来和 AI 棋手对弈是不可能赢得了训练好之后的机器人,最多和棋。

2. 基本要素

该井字棋的强化学习过程:先生成两个 AI 棋手进行对弈,用于训练的对弈棋手的探索率 ϵ \epsilon ϵ 不为 0 0 0,当训练好对弈策略之后,把探索率设置为 0 0 0,测试对弈的 AI 棋手基于训练好的策略赢棋的概率。最后创建一个 HumanPlayer 来代表用户玩家与 AI 棋手进行对局,该“棋手”的执行动作由玩家手动输入。

2.1 棋盘状态

首先先介绍下 State 棋盘状态类型,每个状态(对象)表示的是棋盘的一种布局。具体到棋盘上的每一格,共有 3 3 3 种状态,分别是空格(取值 0 0 0),棋手双方的棋子(取值 1 1 1 或者 − 1 -1 1),一共有 9 9 9 个格子,则棋盘状态空间大小为 3 9 = 19683 3^9=19683 39=19683,但由于是对弈双方一人下一步,且每次只能挑空白的格子下,因此实际的状态空间会更小,可以通过递归的方式生成所有的状态空间,并存放在 all_states 字典当中,键是下面会提到的状态哈希值,字典值存放的是 State 对象,以及该状态是否结束的布尔值。

棋盘的状态设置了 4 4 4 个成员属性,首先是棋盘布局 data,由每个格子的状态值组成一个 3 × 3 3\times 3 3×3 的数组;winner 记录该棋盘状态下是否有赢家;end 记录该棋盘状态是否结束(分出赢家或者和棋);hash_val 则是一个唯一表征该棋盘状态的哈希值,目的是为了方便通过该值进行存储和检索。

class State:
    def __init__(self):
        self.data = np.zeros((BOARD_ROWS, BOARD_COLS))
        self.winner = None
        self.hash_val = None
        self.end = None

棋盘状态类还包含如下的成员方法,每个方法的逻辑如下:

(1)hash():计算哈希值

该方法基于棋盘布局,计算出一个能唯一表征棋盘的值,用于后续能够作为哈希表的键,具体逻辑如下:

h a s h = ( ( i 1 × 3 + i 2 ) × 3 + i 4 . . . . ) × 3 + i 9 hash = ((i_1 \times 3 + i_2)\times 3 + i_4....)\times 3 +i_9 hash=((i1×3+i2)×3+i4....)×3+i9

该哈希值的计算函数既考虑了每个格子的取值大小,也考虑每个值在棋盘当中的位置,能够保证不论是值的差异还是值位置差异,都会改变哈希值。

def hash(self):
    if self.hash_val is None:
        self.hash_val = 0
        for i in self.data.reshape(BOARD_ROWS * BOARD_COLS):
            if i == -1:
                i = 2
            self.hash_val = self.hash_val * 3 + i
    return int(self.hash_val)

(2)iS_end() :判断是否结束

当前棋盘状态结束的条件只有两种,一种是某一方赢棋,将三个相同值的棋子连成线(按行、按列、按对角线),另一种是没人赢棋,且所有的空格都被占满,此时视为和棋。除了这两种情况,其他情况的棋局都没有结束。

# check whether a player has won the game, or it's a tie
def is_end(self):
    if self.end is not None:
        return self.end
    results = []
    # check row
    for i in range(0, BOARD_ROWS):
        results.append(np.sum(self.data[i, :]))
    # check columns
    for i in range(0, BOARD_COLS):
        results.append(np.sum(self.data[:, i]))

    # check diagonals
    results.append(0)
    for i in range(0, BOARD_ROWS):
        results[-1] += self.data[i, i]
    results.append(0)
    for i in range(0, BOARD_ROWS):
        results[-1] += self.data[i, BOARD_ROWS - 1 - i]

    for result in results:
        if result == 3:
            self.winner = 1
            self.end = True
            return self.end
        if result == -3:
            self.winner = -1
            self.end = True
            return self.end

    sum = np.sum(np.abs(self.data))
    if sum == BOARD_ROWS * BOARD_COLS:
        self.winner = 0
        self.end = True
        return self.end

    self.end = False
    return self.end

(3)next_state():棋盘状态转化

在双方棋手每执行一个动作之后,棋盘模型的状态变化是确定,即落子的位置会确定地变为某个状态( 0 , 1 , − 1 0,1,-1 0,1,1),因此棋盘状态的转移概率均为 1 1 1,不存在某个状态在某个动作下会以一定概率转换到不同的几种状态,这种不存在环境随机因素的问题相对简单。

def next_state(self, i, j, symbol):
    new_state = State()
    new_state.data = np.copy(self.data)
    new_state.data[i, j] = symbol
    return new_state

(4)print():可视化输出棋盘状态

为了方便用户在下棋过程中可以直观地看到棋盘的状态,以还原最直接的下棋体验,可以通过该方法将棋盘状态输出成如下的棋盘格式,具体格式可以自定义修改。

def print(self):
        for i in range(0, BOARD_ROWS):
            print('-------------')
            out = '| '
            for j in range(0, BOARD_COLS):
                if self.data[i, j] == 1:
                    token = '*'
                if self.data[i, j] == 0:
                    token = '0'
                if self.data[i, j] == -1:
                    token = 'x'
                out += token + ' | '
            print(out)
        print('-------------')

在这里插入图片描述

2.2 智能体(棋手)

其次介绍模拟下棋的智能体 Player 类,每个智能体对象在对弈的时候,会根据对已知的状态价值的估计,来决定下一步的动作,由于棋盘状态的奖励非常确定,智能体只对下一步动作之后生成的状态价值进行判断,但这可能会错过当前步的状态价值并不是最高,但总的动作路线更优的情况。因此在训练阶段,为每个棋手增加了探索率 ϵ = 0.1 \epsilon=0.1 ϵ=0.1,即在每次决定下一步动作时,有一定概率不按照状态价值进行判断,而随机选择一个动作。

智能体类 Player 6 6 6 个成员属性,分别为:estimations 表示智能体对所有棋盘状态的价值估计;step_size 是更新价值估计的学习率;epsilon 是棋手随机落子的概率;states 是棋手在下棋过程中,棋盘状态的每一次变化;greedy 是棋盘每一次变化时,该棋手是否是选择了当前看来最优的落子位置,是的话值为 1 1 1,反之值为 − 1 -1 1,意味着棋手在当前步是随机落子的;symbol 是下棋双方的编号,值为 1 1 1 的是先手落子,值为 − 1 -1 1 的是后手落子。

class Player:
    def __init__(self, step_size=0.1, epsilon=0.1):
        self.estimations = dict()
        self.step_size = step_size
        self.epsilon = epsilon
        self.states = []
        self.greedy = []
        self.symbol = None

智能体(棋手)类还包含如下的成员方法,每个方法的逻辑如下:

(1)reset()set_symbol():重置智能体的相关属性

由于创建的两个智能体要重复地进行对弈,因此在每次进行对弈之前都需要对智能体中存放历史对弈情况的属性进行重置,包括 statesgreedysymbol,和智能体对各个棋盘状态的价值估计 estimations

def reset(self):
    self.states = []
    self.greedy = []

def set_symbol(self, symbol):
    self.symbol = symbol
    for hash_val in all_states.keys():
        (state, is_end) = all_states[hash_val]
        if is_end: 
            if state.winner == self.symbol:
                self.estimations[hash_val] = 1.0
            elif state.winner == 0:
                self.estimations[hash_val] = 0.5
            else:
                self.estimations[hash_val] = 0
        else:
            self.estimations[hash_val] = 0.5

(2)set_state() 存储棋盘状态

每当棋盘状态发生改变之后,都会将棋盘状态更新到棋手双方的 statesgreedy 属性当中,以便在对弈结束后能通过 backup() 函数对状态价值估计进行更新。

def set_state(self, state):
    self.states.append(state)
    self.greedy.append(True)

(3)act() 基于棋盘状态的下棋策略

前面说到,每次棋盘发生改变时,都会将棋盘状态存储到 states,当轮到某一棋手下棋,则从 states 中取出最后的棋盘状态,并找到该棋盘状态上的所有的空的格子,以一定概率(探索率 ϵ \epsilon ϵ)随机选择落子位置,以 1 − ϵ 1-\epsilon 1ϵ 概率选择落子后棋盘状态价值最大的位置。

def act(self):
    state = self.states[-1] 
    next_states = []
    next_positions = []
    for i in range(BOARD_ROWS):
        for j in range(BOARD_COLS):
            if state.data[i, j] == 0:
                next_positions.append([i, j])
                next_states.append(state.next_state(i, j, self.symbol).hash())

    if np.random.rand() < self.epsilon:
        action = next_positions[np.random.randint(len(next_positions))]
        action.append(self.symbol)
        self.greedy[-1] = False
        return action

    values = []
    for hash, pos in zip(next_states, next_positions):
        values.append((self.estimations[hash], pos))
    np.random.shuffle(values)
    values.sort(key=lambda x: x[0], reverse=True)
    action = values[0][1]
    action.append(self.symbol)
    return action

(4)backup()更新状态价值估计

按照最开始的状态价值估计,每次挑选落子后棋盘状态最大的位置(贪心),或者是用随机的方式落子(随机)。如果是贪心的方法,且状态价值提升 0.5 → 1 0.5\rightarrow1 0.51,则加强该状态的价值估计,因为碰到这个状态则只差一步就能取胜;反之,如果价值状态下降 0.5 → 0 0.5\rightarrow 0 0.50,则会降低该状态的价值估计,只差一步就输棋。具体更新过程如下:

q ( s t ) = q ( s t ) + α ( q ( s t + 1 ) − q ( s ) ) q(s_t)=q(s_t)+\alpha(q(s_{t+1})-q(s)) q(st)=q(st)+α(q(st+1)q(s))

def backup(self):
    self.states = [state.hash() for state in self.states]
    for i in reversed(range(len(self.states) - 1)):
        state = self.states[i]
        td_error = self.greedy[i] * (self.estimations[self.states[i + 1]] - self.estimations[state])
        self.estimations[state] += self.step_size * td_error

这里的状态价值函数更新只考虑当前动作的现有价值,以及下一个状态的估计价值,忽略了再往后的棋盘状态的价值,主要是由于三格井字棋的简易性和特殊性,能在一步内决定赢或者输,此时的奖励衰减因子 γ = 0 \gamma=0 γ=0。而学习率能加快棋手策略的收敛,多次训练后面对相同棋盘状态总能采用相同的动作。

(5)save_policy()load_policy():读写状态价值估计(策略)

当训练好智能体对各个状态的价值估计函数后,即获得了智能体的对局策略。为了保持策略的持久化,保证程序重启或退出时,已训练好的策略也不会丢失,避免从头开始训练,甚至于方便将策略分享出去,在各个不同的环境下进行测试验证,都离不开对训练好的策略进行本地化存储。具体的存储和加载操作如下:

def save_policy(self):
   with open('policy_%s.bin' % ('first' if self.symbol == 1 else 'second'), 'wb') as f:
        pickle.dump(self.estimations, f)

def load_policy(self):
    with open('policy_%s.bin' % ('first' if self.symbol == 1 else 'second'), 'rb') as f:
        self.estimations = pickle.load(f)

2.3 用户交互接口

强化学习代码会创建两个智能体进行对弈训练,当训练结束后,需要由用户作为其中的一方棋手,为了保持对弈程序的正常使用,需要模拟一个用户智能体的类 HumanPlayer 来进行对弈,只不过该智能体的一些属性和方法由用户的操作替代,例如,该对象没有存储对各个状态的价值估计,因为用户本身就有自己的策略。具体与 Player 类相似,如下:

class HumanPlayer:
    def __init__(self, **kwargs):
        self.symbol = None
        self.keys = ['q', 'w', 'e', 'a', 's', 'd', 'z', 'x', 'c']
        self.state = None
        return

    def reset(self):
        return

    def set_state(self, state):
        self.state = state

    def set_symbol(self, symbol):
        self.symbol = symbol
        return

    def backup(self, _):
        return

    def act(self):
        self.state.print()
        key = input("Input your position:")
        data = self.keys.index(key)
        i = data // int(BOARD_COLS)
        j = data % BOARD_COLS
        return (i, j, self.symbol)

注意这里,会通过用户输入的关键字符,来映射用户所要落子的位置,具体也可以通过行列值来输入想要落子的位置,只要能唯一指代棋盘上个各个位置即可。

在这里插入图片描述

3. 训练过程

这里强化训练的关键是,先让两个棋手对弈,然后根据它们的对弈结果,来回过头来调整它们的下棋策略,循环反复,直到达到了最大的训练代数。因此先来看是如何让两个智能体模拟对弈的过程的。

3.1 模拟对弈过程

模拟对弈其实可以视为是在实战,从实战中获取训练数据,通过不断地调整来优化棋手的下棋策略,以保证大概率能获得最大的奖励(赢棋或和棋),对于九宫格井字棋规模较小的游戏而言,训练好的 AI 棋手能保证不输棋。

具体的模拟对弈过程 play() 的逻辑如下:

  • 首先初始化一个 Judger 对象,传入两个 Player 对象,根据传入的顺序给两个棋手分别安排下棋的顺序,例如,先手的 symbol 等于 1 1 1,后手的则为 − 1 -1 1,并通过 set_symbol() 初始化每个棋手对棋盘状态的价值估计;
  • 调用 Judger.play() 执行对弈:
    1. 重置两个棋手的历史对弈记录 statesgreedy
    2. 初始化当前棋盘状态为空棋盘;
    3. 通过 alternate() 生成器来轮流取出其中一方棋手来下棋,每个棋手根据自身的状态价值估计来决定下哪里,落子后更新棋盘状态并将该棋盘状态录入两个棋手的状态历史 states,循环往复,直到对弈结束,返回对局的赢家(返回结果为 1 , − 1 , 0 1,-1,0 1,1,0,和棋用 0 0 0 表示)。
class Judger:
    def __init__(self, player1, player2):
        self.p1 = player1
        self.p2 = player2
        self.current_player = None
        self.p1_symbol = 1
        self.p2_symbol = -1
        self.p1.set_symbol(self.p1_symbol)
        self.p2.set_symbol(self.p2_symbol)
        self.current_state = State()    

    def reset(self):
        self.p1.reset()
        self.p2.reset()

    def alternate(self):
        while True:
            yield self.p1
            yield self.p2

    def play(self, print=False):
        alternator = self.alternate()
        self.reset()
        current_state = State()
        self.p1.set_state(current_state)
        self.p2.set_state(current_state)
        while True:
            player = next(alternator)
            if print:
                current_state.print()
            [i, j, symbol] = player.act()
            next_state_hash = current_state.next_state(i, j, symbol).hash()
            current_state, is_end = all_states[next_state_hash]
            self.p1.set_state(current_state)
            self.p2.set_state(current_state)
            if is_end:
                if print:
                    current_state.print()
                return current_state.winner

3.2 策略的训练及检验

前面提到,训练过程首先会创建两个 AI 棋手,然后将这两个对象传入 Judger 进行初始化,此时这两个 Player 会围绕着训练的整个过程,且棋手的状态价值评估函数 estimations 会一直更新下去。

这里的训练过程,一共训练 e p o c h s = 1 0 5 epochs=10^5 epochs=105,并记录下训练过程中双方棋手的赢局次数,每训练一次,调用一次 judger.play(),会重置一次棋手的历史下棋记录,但不会重置棋手的状态价值评估函数,只会根据对弈结束后,回顾每次棋盘状态变化后的价值,来更新对棋盘状态的价值评估。当循环训练结束后,保存两个棋手的对弈策略。

def train(epochs):
    player1 = Player(epsilon=0.01)
    player2 = Player(epsilon=0.01)
    
    judger = Judger(player1, player2)
    player1_win = 0.0
    player2_win = 0.0
    for i in range(1, epochs + 1):
        winner = judger.play(print=False)
        if winner == 1:
            player1_win += 1
        if winner == -1:
            player2_win += 1
        print('Epoch %d, player 1 win %.02f, player 2 win %.02f' % (i, player1_win / i, player2_win / i))
        player1.backup()
        player2.backup()
        judger.reset()
    player1.save_policy()
    player2.save_policy()

注意:根据实际训练过程中打印的结果,可以看到对弈的棋手都有一定的概率赢棋,概率大约在 0.01 ∼ 0.03 0.01\sim0.03 0.010.03,这是由于训练过程中设置了对弈双方的探索率 ϵ = 0.01 \epsilon=0.01 ϵ=0.01 导致的,即有一定概率因随机落子而输棋。因此,为了检验训练后的对弈策略效果,额外安排了 ϵ = 0 \epsilon=0 ϵ=0 的对弈检验。流程与训练过程基本一致,只是在初始化 player 对象时,将已经训练好的对弈策略加载进来,且不再对策略进行调整,具体过程如下。

def compete(turns):
    player1 = Player(epsilon=0)
    player2 = Player(epsilon=0)
    judger = Judger(player1, player2)
    player1.load_policy()
    player2.load_policy()
    player1_win = 0.0
    player2_win = 0.0
    for i in range(0, turns):
        winner = judger.play()
        if winner == 1:
            player1_win += 1
        if winner == -1:
            player2_win += 1
        judger.reset()
    print('%d turns, player 1 win %.02f, player 2 win %.02f' % (turns, player1_win / turns, player2_win / turns))

结果如下,经过 1000 1000 1000 次的检验发现,对弈的双方彼此都不能赢对方,这说明对弈策略是成功的。

1000 turns, player 1 win 0.00, player 2 win 0.00

3.3 用户实践

最后是用户实践,即让人与训练好下棋策略进行 PK,如下代码,创建了 HumanPlayer 的对象代表人,从初始话 judger 可以看出,用户是作为先手下棋的。而每次轮到用户下棋,则会先输出当前的棋局状态,再询问用户的落子位置,循环反复,直到对局结束后,重新开启新的对局。

def play():
    while True:
        player1 = HumanPlayer()
        player2 = Player(epsilon=0)
        judger = Judger(player1, player2)
        player2.load_policy()
        winner = judger.play()
        if winner == player2.symbol:
            print("You lose!")
        elif winner == player1.symbol:
            print("You win!")
        else:
            print("It is a tie!")

该强化学习案例的主程序入口如下:

if __name__ == '__main__':
    train(int(1e5))
    compete(int(1e3))
    play()

以上就是用强化学习训练九宫格井字棋的简单案例的全部内容,后续会继续分享相关有趣的案例,并结合主流的一些强化学习框架进行介绍,欢迎交流~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1583951.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GitLab教程(一):安装Git、配置SSH公钥

文章目录 序一、Git安装与基本配置&#xff08;Windows&#xff09;下载卸载安装基本配置 二、SSH密钥配置 序 为什么要使用代码版本管理工具&#xff1a; 最近笔者确实因为未使用代码版本管理工具遇到了一些愚蠢的问题&#xff0c;笔者因此认为代码版本管理工具对于提高团队…

每日Bug汇总--Day03

Bug汇总—Day03 一、项目运行报错 二、项目运行Bug 1、问题描述&#xff1a;Vue前端项目运行后台管理平台订单管理页面下的三个子页面出现点击不同的路由还是会出现相同的页面&#xff0c;导致页面和数据不匹配 解决办法&#xff1a; 在监听器中添加状态来根据路由地址变化…

2024地平线古月居核心开发者招募

2024地平线&古月居核心开发者招募 机器人&#xff0c;作为一个集成了多学科技术的复杂系统&#xff0c;其开发过程充满了挑战。为了帮助开发者们更好地克服这些挑战&#xff0c;提升项目的开发效率和质量&#xff0c;我们特别推出了[2024地平线&古月居核心开发者招募]…

【Gem5】获取构建教程

gem5-tutorial-hpca-2023 1 介绍 1.1 Gem5是什么1.2 Gem5可以用来做什么1.3 获取并构建gem5 gem5-tutorial-hpca-2023 打开网址&#xff1a; github 创建教程代码空空间 “Code” -> “Codespaces” -> “Create Codespace on master” GitHub Codespaces 是一个由…

DSP笔记6-C2000的中断机制

中断Interrupt&#xff1a; 单核CPU顺序执行程序 中断源&#xff0c;引起计算机中断的时间&#xff0c;解放cpu&#xff0c;提高效率。 三个等级&#xff1a;CPU中断&#xff0c;PIE中断&#xff0c;外设中断 cpu定时器&#xff0c;EPWM&#xff0c;ADC&#xff0c;eCAP&…

git bash用法-批量修改文件名

在win系统上安装git bash可以使用命令行模式操作&#xff0c;比较方便 1.原始文件名 2.代码 for file in *3utr*; do mv "$file" "$(echo "$file" | sed s/3utr/5utr/)"; done3.修改后的文件名

PS入门|学PS一定要先知道图层这玩意儿

前言 开始学习PS的小伙伴肯定是会遇到很多问题&#xff0c;最常见的莫过于为啥我调整了某些参数之后&#xff0c;并没有任何作用。 这个就涉及到图层的问题了。 学PS一定要知道&#xff0c;图层面板怎么看。 正文开始 首先咱们讲的图层面板基本上是在PS里100%会用到的功能。…

solidworks镜像实体怎么用

在SolidWorks中&#xff0c;镜像实体功能用于复制并反转实体或特征&#xff0c;使其沿着指定的基准面对称。以下是使用SolidWorks镜像实体的基本步骤&#xff1a; 1. 打开模型&#xff1a;首先打开SolidWorks软件&#xff0c;并加载您想要镜像的三维实体模型。 2. 找到镜像命…

蓝桥2021A组C题

货物摆放 问题描述格式输入格式输出评测用例规模与约定解析参考程序难度等级 问题描述 格式输入 无 格式输出 输出答案 评测用例规模与约定 无 解析 数字给的相当大所以我们不能直接给他暴力了&#xff0c;不然等很久都跑不出来。由题目我们可以得到让nLxWxH&#xff0c;所…

[leetcode]remove-duplicates-from-sorted-list

. - 力扣&#xff08;LeetCode&#xff09; 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2]示例 2&#xff1a; 输入&…

Linux——线程同步与生产者消费者模型

目录 前言 一、线程同步 二、生产者消费者模型 三、条件变量 1.理解条件变量 2.条件变量接口 2.1 条件变量初始化与销毁 2.2 条件变量等待 2.3 条件变量唤醒等待 2.4 条件变量接口运用 2.5 条件变量进行抢票 3.条件变量的细节 四、基于BlockingQueue的生产者消费者…

SL4010 低压升压恒压芯片 2.7-24V输入 输出30V/10A 300W功率

SL4010是一款高效能、宽电压范围的低压升压恒压芯片&#xff0c;其卓越的性能和广泛的应用领域使其在市场上备受瞩目。该芯片支持2.7-24V的宽输入电压范围&#xff0c;能够提供稳定的30V/10A输出&#xff0c;最大输出功率高达300W&#xff0c;为各种电子设备提供稳定可靠的电源…

蓝桥杯物联网竞赛_STM32L071KBU6_我的全部省赛及历年模拟赛源码

我写的省赛及历年模拟赛代码 链接&#xff1a;https://pan.baidu.com/s/1A0N_VUl2YfrTX96g3E8TfQ?pwd9k6o 提取码&#xff1a;9k6o

还不会免费将PDF转为Word?赶快试试这3种工具!

PDF文档格式转换是高频且刚需的办公需求&#xff0c;虽然很简单&#xff0c;但其实绝大部分人找不到合适的工具。 将PDF免费转为Word的方法有很多&#xff0c;这里主要介绍三种工具。 第一种使用最常见的Word软件&#xff0c;第二种使用免费转换网站pdf2doc&#xff0c;第三种…

算法打卡day41|动态规划篇09| Leetcode198.打家劫舍、213.打家劫舍II、337.打家劫舍 III

算法题 Leetcode 198.打家劫舍 题目链接:198.打家劫舍 大佬视频讲解&#xff1a;198.打家劫舍视频讲解 个人思路 偷还是偷&#xff0c;这取决于前一个和前两个房是否被偷了&#xff0c;这种存在依赖关系的题目可以用动态规划解决。 解法 动态规划 动规五部曲&#xff1a;…

李廉洋:4.9黄金屡创新高。黄金原油晚间最新分析建议。

但当下不管是战争因素所带来的避险情绪影响还是美国降息与否所带来的经济影响都无疑还是支撑着黄金继续走高&#xff0c;那么接下来&#xff0c;只要市场不出现较大的利空影响&#xff0c;黄金都不会有较大的回调力度&#xff0c;所以我们当下不管是短线还是长线仍旧以继续看多…

【LAMMPS学习】八、基础知识(1.6) LAMMPS 与其他代码耦合

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

外包干了3天,技术退步明显.......

先说一下自己的情况&#xff0c;大专生&#xff0c;19年通过校招进入杭州某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了四年的功能测…

蓝桥杯 每日2题 day4

碎碎念&#xff1a;好难好难&#xff0c;&#xff0c;发呆两小时什么也写不出来&#xff0c;&#xff0c;&#xff0c;周六大寄了 10.阶乘约数 - 蓝桥云课 (lanqiao.cn) 暴力跑了两个小时没出来结果&#xff0c;&#xff0c;去看题解要用数学&#xff1a;约数定理&#xff0c…

成功解决> 错误: 无效的源发行版:17

运行项目的时候出现下面的报错&#xff1a; Execution failed for task ‘:device_info_plus:compileDebugJavaWithJavac’. 错误: 无效的源发行版&#xff1a;17 原因&#xff1a;没有设置好自己项目的JDK版本 解决&#xff1a;1.检查自己项目的JDK版本 将自己的项目改为JDK 1…