动态规划(DP)学习和思考

这篇具有很好参考价值的文章主要介绍了动态规划(DP)学习和思考。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、总体说明

最近在备战蓝桥杯,感觉蓝桥杯很多题目都可以尝试用暴力搜索,暴力枚举的方法尝试得出最终的结果,但是暴力的方式效率并不是很高,而且容易超时。在以前学回溯算法的时候,也是一脸懵,不理解IndexStart到底什么意思,不知道如何去找参数和递归逻辑,好在通过慢慢的学习和做题,逐渐对这类算法有了进一步的认识,下次可以单独出一期讲回溯算法。

本次博客主要讲述的是动态规划算法,也就是那个明明知道什么叫动态,什么叫规划,连起来就不知道它到底是什么的算法了。其实本人也还在一个在学习的过程,但通过几天的学习和思考,逐渐对DP有了一个新的认识,以下我将慢慢来进行说明我对DP的一个学习认识过程。

二、动态规划的概念

本人习惯于对任何问题,无论是学高数还是大物,甚至是数据结构与算法,都首先从其最初始的定义开始。那么什么叫动态规划呢?

动态规划(Dynamic Programming,简称DP)是一种解决多阶段决策问题的数学优化方法。它将原问题分解成若干个子问题,通过解决子问题只需解决一次并将结果保存下来,从而避免了重复计算,提高了算法效率。总的来讲,动态规划算法使用时,这个问题得具备两个要素:1.重叠子问题2.最优子结构。

三、具体示例

大体来说,判断一个题目适不适合动态规划,就得去尝试寻找对于这个题目来说,他存不存在最优子结构和重叠子问题。如果确实存在,那么如何去尝试用DP算法去解决这个问题呢?接下来跟着我的思路,去尝试看三个DP的问题,相信你会有更多的收获。

3.1 找出连续子序列的最大和

笔者是通过一个视频认识这道题的,这个视频在哔站,在搜索框中输入动态规划,第一个视频就是它。视频链接在这。10分钟彻底搞懂“动态规划”算法_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1AB4y1w7eT/?spm_id_from=333.337.search-card.all.click&vd_source=09d0a5241128639946dcdfb5bf184df2感兴趣的小伙伴可以去看看,相信会对你有所帮助。

这道题目是这样说的,给定一个一维数组,尝试找出这个一维数组的连续子序列的最大和。如给出一维数组[3,-4,2,-1,2,6,-5,4],这个一维数组的最大连续子序列和为2+(-1)+2+6=9。

那么怎么去做这道题目呢?想开始我拿到这道题目也是一脸懵。一直反复在脑子里面思考“连续子序列的最大和,连续子序列的最大和,连续子序列的最大和”。但是重点在于这个嘛?重点在于你不要被他所谓的标签给束缚了,有人告诉你这道题可以用动态规划去解决,你就反反复复地去尝试用动态规划的思想去解决。首先尝试去定义一个DP数组,确定这个DP数组的意思是什么,然后去找到状态转移方程,然后定义初始化的条件????是这样做的嘛?显然不是。

对于这道题,我认为最好的方式是不要认为他可以用DP去解决,假装你自己并不知道这道题可以用DP,你甚至不知道这道题怎么去解决。到底能用数据结构或者算法中的哪一个去解决,你自己完全不知道。你首先第一步需要做的事情是去分析。这道题目说的是连续子序列,那么我们就从最开始的3开始,3+(-4),3+(-4)+2,3+(-4)+2+(-1).........,一直往后,然后从-4开始,从2开始,你发现了什么?在算3+(-4)+2+(-1)的时候,我是不是算过了3+(-4),我从2开始的话是不是算过了2+(-1)?所以,你发现了什么?一个很长的连续序列长串,他确确实实是由一个个连续序列短串加起来得到的,所以是不是存在子结构?显然,长串的最优解也是由子串的最优解得出,也就是说是存在最优子结构,不用多说,必然也是一个重叠子问题。那么说明了什么,这道题可以用DP的思想来进行解决。

那么DP首先实现三部曲,第一确定你的DP数组的含义,其次找到状态转移方程,接着初始化,最后进行相应的操作,实现整个算法思路和过程就OK了。那么问题来了,DP数组的含义我们怎么去确定呢?我觉得最简单的方式就是,他要求什么,我就设什么。既然这道题目让我求的就是子序列的和,我就定义这个dp数组的值就是子序列的和。那么怎么去确定这个dp数组是一维还是二维呢,也就是具体的含义是什么?这就得看你上面的分析了,第一个重点在于你的连续子序列从哪里开始,也就是数组的索引值是什么,其次在于你的连续子序列的长度是什么,所以这个dp就是一个二维数组,因为你的这个子序列的值和这两个因素有关。那么我们就可以定义dp[i][j]的含义为从数组的索引值为i处开始的长度为j的连续子序列的和。

那么问题来了,这个状态转移方程式什么呢?其实你加以分析一下,你要分析,要去动手,不然永远凭脑子去想的话永远都想不出来。dp[i][j]表示数组的索引值为i处开始的长度为j的连续子序列的和,那么dp[i-1][j]等于什么呢?dp[i][j-1]的含义是什么?索引值为i处开始的长度和j-1的连续子序列的和,所以dp[i][j]=dp[i][j-1]+arr[i+j-1]。这里把原数组定义为arr。

dp数组定义和状态转移方程已经写完了,那么接下去就是去实现dp数组的初始化。你可以将dp数组都初始化为0,也可以dp[0][1]=arr[0]都可以。这其实是和你的状态转移方程有一定的联系,可以多思考思考,初始化很重要。

代码的具体实现如下:

#include <iostream>
#include <vector>
using namespace std;

int dp[50][50];
vector<int> arr;


int main()
{
	int n, result;
	result = 0;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		int num;
		cin >> num;
		arr.push_back(num);
	}

	for (int i = 0; i < n; i++)
	{
		for (int j = 1; j <= n - i; j++)
		{
			dp[i][j] = dp[i][j - 1] + arr[j + i - 1];
			if (dp[i][j] > result)
			{
				result = dp[i][j];
			}
		}
	}

	cout << result << endl;

	return 0;
}

运行结果如下:

动态规划(DP)学习和思考,c++,算法,动态规划

3.2 数组分割

我们再来看一道蓝桥杯真题。一样的,我们假装不知道这题目可以用动态规划来解决,通过自己的分析和探索,来看看这道题的原理和内层逻辑是什么,可不可以用动态规划去实现,如果可以用动态规划去实现,我们该怎么去实现?

具体题目如下:

动态规划(DP)学习和思考,c++,算法,动态规划

题目链接为用户登录https://www.lanqiao.cn/problems/3535/learning/

遇到任何题目,第一步最重要的不是看他到底是用什么算法去解决,最重要的点在于自己去分析。分析问题,看看这道题适合用什么方式去解决。所以,让我们开始动手去做,去尝试,看看怎么具体解决这个问题。

首先理解题目意思,先不管有几组数据,我们先尝试一组数据。给出一个数组,随便在这个数组里面选择数字,使数字之和为一个偶数,有多少种不同的选择方式。当然,需要保证S1、S2都为偶数,那么第一,整个数组的和是不是得为偶数。这就出现了第一个判断的条件了,脑子就可以开始写if判断了。

接下来,继续去思考,假如说对于一个数组[1,3,5,7,12,6,8]。我需要随便找数字,让他的和为偶数,我该怎么去找呢?可以选择1,可以选择3,可以选择5,甚至是7,这就显的很杂乱无章。如果我选择了1+3+5,那么后面再遇到1+3+5+7的时候,我是不是可以用前面已经算好的1+3+5得到的结果,再去加7呢?当然可以,那么就往这个方向去思考,存在子结构,重叠子问题,可以尝试。那么dp数组的定义就是所选数字之和,但是有个新问题,和找连续子序列的最大和这个问题不一样,他是一个连续的子序列,我这里是不连续的,是可以随机取的,是无序的,这里的i,j就无法定义,或者说定义了,你也没法找到状态转移方程。所以这道题,和以前写过的都不一样。

那么我们该怎么去思考呢?尝试从问题的结果出发,去寻找问题。这道题的问题是什么?是让我们去求选择的R1有多少种可能的情况。好,那我们这样来思考,既然这个数组他很长,那么我们就选择其中i个数吧,反正i小于等于这个数组的长度。从i个数中随便选择数,使选择的数之和为偶数的方式有x种,那你再去思考一下,我这时候再从整个数组中(除去那已经拿出的i个数)拿出一个数,对于这个数来说,是不是有两种可能,第一种,选择这个数,第二种,不选择这个数。那如果你这个数他是偶数,前面i个数中随便选择的数之和也为偶数,偶数+偶数当然是符号条件的,所以你把这个数加入i个数中,那么在i+1个数中任意选择数,使之和为偶数的情况有多少种?思考下,没错,就是2x种。同样,如果是奇数,可以自行按着这个思路进行思考下去,我就不再赘述。

那么思考完了之后,你就会发现,这不是一个动态规划的思路嘛?存在最优子结构,也是一个最优子问题,还有相应的状态转移方程,所以就是一个DP问题。

那么开始吧,我们尝试去写代码实现,具体代码如下:

#include <iostream>
#include <vector>
using namespace std;

//dp[i][0] 从数组前面i个数任意组合,得到结果和为偶数记为0,dp的值为从数组前面i个数任意组合,得到结果和为偶数的子集个数
//dp[i][1] 从数组前面i个数任意组合,得到结果和为奇数记为1,dp的值为从数组前面i个数任意组合,得到结果和为奇数的子集个数
//如果arr[i]为偶数,那么dp[i+1][0]=dp[i][0]*2
//如果arr[i]为奇数,那么dp[i+1][0]=dp[i][0]+dp[i][1]
long long mod = 1e9 + 7;

int main()
{
	int n, q;
	cin >> n;
	q = 0;
	vector<int> storge(n);
	while ( q < n)
	{
		int m, sum;
		cin >> m;
		vector<int> arr;
		sum = 0;
		for (int i = 0; i < m; i++)
		{
			int num;
			cin >> num;
			sum += num;
			arr.push_back(num);
		}
		
		if (sum % 2 == 0)
		{
			int dp[1010][2];
			dp[0][0] = 1;
			dp[0][1] = 0;
			for (int i = 1; i <= m; i++)
			{
				if (arr[i-1] % 2 == 0)
				{
					dp[i][0] = 2 * (dp[i - 1][0]) % mod;
					dp[i][1] = 2 * (dp[i - 1][0]) % mod;
				}
				else
				{
					dp[i][0] = (dp[i - 1][0] + dp[i - 1][1]) % mod;
					dp[i][1] = (dp[i - 1][0] + dp[i - 1][1]) % mod;
				}
			}
			storge[q] = dp[m][0];
		}
		else
		{
			storge[q] = 0;
		}

		q += 1;
	}

	for (int i = 0; i < q; i++)
	{
		cout << storge[i] << endl;
	}

	return 0;
}

 测试用例运行效果截图:

动态规划(DP)学习和思考,c++,算法,动态规划蓝桥测试用例只通过了20%,最近一直苦恼于样式用例好像都能通过,但是一旦测试用例,就有一部分能过,或者都不能过这种。有知道为什么的小伙伴也可以写在评论区或者私信我,告诉我一下怎么解决这个问题。(我觉得我写的这个代码的思路是没有问题的。)

蓝桥测试截图:

动态规划(DP)学习和思考,c++,算法,动态规划

3.3 保险箱问题

题目链接在这

用户登录https://www.lanqiao.cn/problems/3545/learning/首先一样的,先假装不知道这道题能用什么方式去解决,去动脑子,去思考,尝试对问题从最基础的地方开始剖析。

一个保险箱有n个数字,x为初始输入数字,y为我们需要变成的数字。根据题目的意思,实际上也就是说anan-1an-2......a0其实也就是为an*pow(10,n-1)+.....a0,所以第一个性质,无所谓你从哪个数字开始进行调整,可以是an,也可以是an-1,你随便调整。比如对于所给样例,输入12349,得到54321,你可以选择从1开始先调整到5,也可从2开始调整到4,依次类推都行。

既然怎么调整都行,我们尽量做到有序,那么要保证有序,可以从左到右,或者从右往左。假如从左往右去进行调整,思考一个问题,对于一个数,比如从9变成1,有两种可能,第一种我直接给9+2,是不是可以将这个位上的数变成1,或者说我从9直接-8也能变成1,反正不管怎么样,我要将x上的某一位变成y上的某一位,无非就是这两种操作。既然如此如果从左往右开始进行,如果对于第二位你进行了进位操作,势必会影响到第一位,那么第一位刚处理完岂不是白处理了。但是反过来,你从末尾开始从右往左进行调整,你可以发现,我只要调整好了这一位,影响的是前一位,但是前一位并没有进行调整过,因此,我们选择从右开始向左调整。

所以经过分析,我觉得可以用暴力搜索的方式去尝试解决这个问题,事实上递归的深度也就是整个数字的个数n,每一次操作,都有两种可能,要么进位/退位,要么直接在对应位置上进行+/-的操作。

以下是我实现的dfs的代码:

#include <iostream>
#include <string>
using namespace std;

int n;//数字位数
string x, y;//x为输入数字,y为目标数字
int arr[50];//1代表该数字直接加/减,2代表该数字进位/退位

int ninef(int a, int b, int flag)
{
	int res;

	//flag为1,说明直接进行加/减
	if (a > b && flag == 1)
	{
		res = a - b;
		return res;
	}
	//flag为1,进行退位/进位操作
	if (a > b && flag == 2)
	{
		res = 10 + b - a;
		return res;
	}

	if (a < b && flag == 1)
	{
		res = b - a;
		return res;
	}

	if (a < b && flag == 2)
	{
		res = 10 + a - b;
		return res;
	}
	
}


void dfs(int startIndex,int result, int& max_num)
{
	if (startIndex < 0)
	{
		if (result < max_num)
		{
			max_num = result;
		}
		return;
	}

	int a = x[startIndex] - '0';
	int b = y[startIndex] - '0';
	//递归逻辑
	if (a > b)
	{
		arr[startIndex] = 1;
		result += ninef(a, b, 1);
		dfs(startIndex - 1, result, max_num);
		arr[startIndex] = 0;
		result -= ninef(a, b, 1);

		arr[startIndex] = 2;
		result += ninef(a, b, 2);
		if (startIndex != 0)
		{
			x[startIndex - 1] += 1;
			
		}	
		dfs(startIndex - 1, result, max_num);
		arr[startIndex] = 0;
		result -= ninef(a, b, 2);
		if (startIndex != 0)
		{
			x[startIndex - 1] -= 1;

		}
	}

	if (a < b)
	{
		arr[startIndex] = 1;
		result += ninef(a, b, 1);
		dfs(startIndex - 1, result, max_num);
		arr[startIndex] = 0;
		result -= ninef(a, b, 1);

		arr[startIndex] = 2;
		result += ninef(a, b, 2);
		if (startIndex != 0)
		{
			x[startIndex - 1] -= 1;
			
		}
		dfs(startIndex - 1, result, max_num);
		arr[startIndex] = 0;
		result -= ninef(a, b, 2);
		if (startIndex != 0)
		{
			x[startIndex - 1] += 1;

		}
	}
	if (a == b)
	{
		dfs(startIndex - 1, result, max_num);
	}
}

int main()
{
	cin >> n;
	cin >> x >> y;
	int max_num = 100000;
	int result = 0;
	dfs(n - 1, result, max_num);

	cout << max_num << endl;
	
	return 0;
}

运行样例如下:

动态规划(DP)学习和思考,c++,算法,动态规划

样例运行没问题,再蓝桥官方运行的话只通过了百分之二十的样例动态规划(DP)学习和思考,c++,算法,动态规划

既然如此,通过上述分析,其实不难发现一个DP的思想在里面,且听我慢慢道来。

我以简单一点的实例进行说明。

n=3 ,x=129 ,y=531,结合上述分析,不难画出如下图所示的树状结构图

动态规划(DP)学习和思考,c++,算法,动态规划

剩下的节点可以自行画完。整个最小方案就是9+2,然后3不变,最后1变5,最小方案为6。其实对每个结点来说,无非是两种情况,到底是+/-得来的,还是进位/退位得来的呢?这里以x和y的从左往右数的第二位来说明吧。假设现状9已经变为了1,那么9怎么样变为1呢?是+还是减?+了必然会影响他的前一位,减了并不影响前一位,所以我第二位有几种变来的方式?取决于上一位怎么变化。同时,通过树结构不难发现,整个最优解其实就在一条路径上,你会发现,整体的最优解,其实也就是每一步的最优方案。对于9而言,总体最优的方案就是9+2,而实际单独拿出9来说,其最优解也是对9+2,变成1。那么存在最优子结构,存在重叠子问题,又何尝不是一个DP问题呢。

那么既然整道题目是一个DP问题,首先得确定DP数组的含义到底是什么。回到树结构,观察每一个结点,DP问题不就是找到前后之间的联系嘛,试着去找一下。第一个节点有两种方式变为目标节点,但实际上他自己如何变会影响到下一节点,那么这就是前后之间的关系。那么就可以定义了,DP[i][j]为目标为i的节点通过j的方式得到,而j有什么可能,要么+,要么-,所以j的值可为0/1。具体代码如下:

#include<bits/stdc++.h>

using namespace std;

int n;

int x[100010];//存放输入:1 2 3 4 9
int y[100010];//存放答案:5 4 3 2 1

int dp[100010][2];//dp[i,j]表示(从后向前数)第i位达到了目标,且第i位是靠+实现的(y==0)或者第i位是靠-实现的(y==1)
 


void fun()
{
    dp[0][0] = (y[0]-x[0]+10)%10;//9加到2
    dp[0][1] = (x[0]-y[0]+10)%10;//9减到2
    
    for( int i=1;i<=n-1;i++ )//以i=1为例: 无非就是当前是加减,上一位是加减,四种排列组合状态 
    {
        dp[i][0] = min( (y[i] - x[i] - (y[i-1]<x[i-1]) +10 )%10 + dp[i-1][0],//(2-4-(1<9)+10)%10+2 = 9:9先加到1,5再加到2 
                         (y[i] - x[i] + (y[i-1]>x[i-1]) +10 )%10 + dp[i-1][1]);//(2-4+(1>9)+10)%10+7 = 16:9先减到1,4再加到2 
        
        dp[i][1] = min( (x[i] - y[i] + (y[i-1]<x[i-1]) +10 )%10 + dp[i-1][0],//(4-2+(1<9)+10)%10+2 = 5: 9先加到1,5再减到2 
                         (x[i] - y[i] - (y[i-1]>x[i-1]) +10 )%10 + dp[i-1][1]);//(4-2-(1>9)-2+10)%10+2 = 10:9先减到1,4再减到2 
    }
    
    cout<<min( dp[n-1][0],dp[n-1][1] )<<endl;//最终的结果,可能是加来的,也有可能是减来的。 
}


int main()
{
    cin>>n;
    char c;
    for( int i=n-1;i>=0;i-- )
    {
        cin>>c;
        x[i] = c-'0';
    }
    
    for( int i=n-1;i>=0;i-- )
    {
        cin>>c;
        y[i] = c-'0';
    }
    
    fun(); 
    
}

四、总结

动态规划算法的变形有很多,题型有很多,解题的方法也有很多。想要把动态规划算法用的如鱼得水,我觉得还是得多刷题目并不断思考。

学习算法时,重点不在于这道题的标签是什么,更重要的在于去思考这道题目的底层逻辑,尝试去对问题进行剖析,而不是停留在用DP或者回溯或者别的算法的公式去套。题目是活的,人也是活的,希望我们都能在不断解决问题的快乐中继续前行。文章来源地址https://www.toymoban.com/news/detail-845405.html

到了这里,关于动态规划(DP)学习和思考的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • ★动态规划(DP算法)详解

    什么是动态规划:动态规划_百度百科 内容太多了不作介绍,重点部分是无后效性,重叠子问题,最优子结构。 问S-P1和S-P2有多少种路径数,毫无疑问可以先从S开始深搜两次,S-P1和S-P2找出所有路径数,但是当这个图足够大,那就会超时。 动态规划旨在用 空间换时间 ,我们

    2024年02月04日
    浏览(41)
  • 算法——动态规划(DP)——递推

    动态规划常用于解决优化问题。 动态规划通常以自底向上或自顶向下的方式进行求解。 自底向上的动态规划从最简单的子问题开始,逐步解决更复杂的问题,直到达到原始问题。 自顶向下的动态规划则从原始问题出发,分解成子问题,并逐步求解这些子问题。 动态规划算法

    2024年01月20日
    浏览(46)
  • 动态规划(DP)(算法笔记)

    本文内容基于《算法笔记》和官方配套练题网站“晴问算法”,是我作为小白的学习记录,如有错误还请体谅,可以留下您的宝贵意见,不胜感激。 动态规划(Dynamic Programming,DP)是一种用来解决一类最优化问题的算法思想。简单来说,动态规划将一个复杂的问题分解成若干个子

    2024年02月05日
    浏览(38)
  • C++动态规划-线性dp算法

    莫愁千里路 自有到来风 CSDN 请求进入专栏                                    X 是否进入《 C++ 专栏》? 确定 目录  线性dp简介 斐波那契数列模型  第N个泰波那契数 思路: 代码测试:  三步问题 思路: 代码测试: 最小花费爬楼梯 思路: 代码测试:  路径问题 数字三

    2024年02月19日
    浏览(38)
  • 【算法】动态规划(dp问题),持续更新

    介绍本篇之前,我想先用人话叙述一般解决动态规划问题的思路: 动态规划的问题,本身有许多产生结果的可能,需要在具体题目下得到满足某个条件的解。 如何得到呢? 我们就需要根据这个具体问题,建立一个状态表( dp 表 ),在这张 dp 表中的每一个位置的数据都有明

    2024年02月04日
    浏览(40)
  • 算法套路十三——动态规划DP入门

    动态规划和递归都是通过将大问题分解为较小的子问题来解决问题。它们都可以用来解决具有重叠子问题和最优子结构特性的问题。 递归是一种自顶向下的方法, 它从原始问题开始 ,递归地将问题分解为较小的子问题dfs(i)—— dfs(i)代表的是从第i个状态开始进行递归求解能

    2024年02月15日
    浏览(48)
  • c++ 算法之动态规划—— dp 详解

    dp 是 c++ 之中一个简单而重要的算法,每一个 OIer 必备的基础算法,你知道它究竟是什么吗? 目录 一、简介         1.为什么不能贪心?         2.揭秘 dp   二、应用         1.定义         2.边界值         3.动态转移方程         4.结果         5.代码

    2024年04月09日
    浏览(40)
  • 算法——动态规划(DP,Dynamic Programming)

    动态规划常用于解决优化问题。 动态规划通常以自底向上或自顶向下的方式进行求解。 自底向上的动态规划从最简单的子问题开始,逐步解决更复杂的问题,直到达到原始问题。 自顶向下的动态规划则从原始问题出发,分解成子问题,并逐步求解这些子问题。 动态规划算法

    2024年02月02日
    浏览(46)
  • acwing算法基础之动态规划--线性DP和区间DP

    线性DP:状态转移表达式存在明显的线性关系。 区间DP:与顺序有关,状态与区间有关。 题目1 :数字三角形。 解题思路:直接DP即可, f[i][j] 可以来自 f[i-1][j] + a[i][j] 和 f[i-1][j-1] + a[i][j] ,注意 f[i-1][j] 不存在的情况(最后一个点)和 f[i-1][j-1] 不存在的情况(第一个点)。

    2024年02月04日
    浏览(40)
  • Day36算法记录|动态规划 dp02

    步骤回顾: C语言版本写的很清楚 对应得Java版本视频解析 方法一: 动态规划 1 确定dp数组(dp table)以及下标的含义 dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 2 . 确定递推公式 ,求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。 3. dp数

    2024年02月12日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包