算法训练题集(自用)

Leetcode上的一部分DP模板

2021.8.9 DP:

DP题一定要分析状态转移方程

递归重复调用时间复杂度高,迭代开数组空间复杂度高(未优化),通过降维优化可以将空间复杂度降低

https://www.luogu.com.cn/problem/P1060
https://www.luogu.com.cn/problem/P1064 稍微复杂一些的变体

https://www.luogu.com.cn/problem/P1164

2021.8.10 线性DP:

新知识:

  • 最大上升子序列
  • Dilworth定理:把一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度
//求最大上升子序列长度
for (i = 1; i <= n; i++)//求前i个数中 每个数的最长不升子序列
	{
		for (j = 1; j < i; j++)//前i个数一一比较大小
		{
			if (a[j] < a[i])
			    //比较i原本的序列长度与j序列长度加1(加上i后的长度)后的大小
				dp[i] = max(dp[i], dp[j] + 1);    //转移状态方程
		}
		//因为序列越往后越长,所以只需依次向后比较
		length = max(ret, dp[i]);
	}

导弹拦截问题:https://www.luogu.com.cn/problem/P1020 O(nlogn)方法未理解
变体:https://www.luogu.com.cn/problem/P1091

2021.8.10 线性DP:

不知道这段话是不是正确的,姑且是目前为止自己对线性动态规划的理解: 将每一个结果拆解为 该结果的上一步+本步,递推。关键点有两个:

  1. 已知晓最初始的状态/值
  2. 子问题不会反复重复调用

确定了这两点后就可以尝试列出状态转移方程求解。

https://www.luogu.com.cn/problem/P1280

//对于每一个时间点,如果没有此时开始的任务,视为摸了;如果有此时开始的任务,则和任务执行完后的时间点比较,若完成任务后能摸得更久,则做任务,反之摸了。
//对于【虽然没有此刻任务开始,但有任务正在执行】的时间点,由于比较时只比较起始与结束点,并不考虑过程,所以可以临时视为摸了,并不影响其余点的计算
//由于前面的空闲时间回由于后续的任务选择时刻而实时变化,所以不能正向搜索;应当首先确定最后一分钟能不能摸鱼,随后往前递推

//

	for (i = 1; i <= k; i++)
	{
		cin >> z[i].ks >> z[i].js;
		sum[z[i].ks]++;
	}
	sort(z + 1, z + k + 1, cmp);//保险起见按开始时间给任务排序,因为是倒着搜,所以开始时间晚的排在前面
	for (i = n; i >= 1; i--)//倒着搜   
	{
		if (sum[i] == 0)
			f[i] = f[i + 1] + 1;//摸了
		else for (j = 1; j <= sum[i]; j++)
		{
			if (f[i + z[num].js] > f[i])
				f[i] = f[i + z[num].js];
			num++;//当前已扫过的任务数   
		}
	}
	cout << f[1] << endl;

2021.9.23 线性DP:

隔了一个多月终于又开始写了。

https://www.luogu.com.cn/problem/P1140
根据测试数据大小选择递归/迭代:本题最开二维数组仅100*100,空间较小;递归时间复杂度相当高。尽量选择迭代方法。
注意边界情况的判断。
题解有很有参考价值:https://www.luogu.com.cn/paste/u7l8dqnn

区间动态规划:

核心为分治思想,由小区间最优解合并成原区间最优解。

1.状态:用dp[i][j]表示区间(i,j)的最优解

2.状态转移:

    最常见的写法:dp[i][j]=max/min(dp[i][j],dp[i][k]+dp[k+1][j]+something)

    理解:区间(i,j)的最优解就等于【区间(i,j)】与【(i,k)和区间(k+1,j)合并后的值】相比谁更优。

3.初始化: dp[i][i]=0/other (i=1->n)

 只包含自己本身的时候,也就是区间长度为1,值肯定是确定的。

原文链接:https://blog.csdn.net/u012679087/article/details/84109739
————————————————

一般的模板:

for (int len=2;len<=n;len++)//len:长度 ,l:左区间断点,r:又区间端点
    for (int l=1;l+len-1<=n;l++) //保证(l,r)的长度为len,且不越界
    {
        int r=l+len-1;
        for (int k=l;k<=r;k++)
            dp[l][r]=max/min(dp[l][r],dp[l][k]+dp[k+1][r]+something)
    }

经典例题:https://www.luogu.com.cn/problem/P1880

//转移方程1:f(i, j)=max/min(f(i, j),f(i, k)+f(k+1, j))
//转移方程2:f(i, j) = max{ f(i,k) + f(k + 1,j) } +sum(i, j)
//本质上为一个方程,拆解成两段便于理解
int main()
{
	int n, i, j, k;
	int a[210];//原数组
	int sum[220] = { 0 };//记录每次得分,第i到第j堆石子累积得分为sum[j] + sum[i-1]
	cin >> n;
	for (i = 1; i <= n; i++)
	{
		cin >> a[i]; 
		a[i + n] = a[i];
	}
	for (i = 1; i <= n * 2; i++)
		sum[i] = sum[i - 1] + a[i];
	//输入结束,开始dp
	for (int len = 2; len <= n; len++)
	{
		for (i = 1; i + len - 1 <= 2 * n; i++)
		{
			j = i + len - 1;
			f1[i][j] = 0x3f3f3f3f;
			f2[i][j] = 0;
			for (k = i; k < j; k++)
			{
				//转移方程1
				f1[i][j] = min(f1[i][k] + f1[k + 1][j], f1[i][j]);
				f2[i][j] = max(f2[i][k] + f2[k + 1][j], f2[i][j]);
			}
			//转移方程2
			f1[i][j] += sum[j] - sum[i - 1];
			f2[i][j] += sum[j] - sum[i - 1];
		}
	}
	int M = -1;
	int m = 0x3f3f3f3f;
	for (i = 1; i <= n; i++)
	{
		if (f1[i][i + n - 1] < m) m = f1[i][i + n - 1];
		if (f2[i][i + n - 1] > M) M = f2[i][i + n - 1];
	}
	cout << m << endl << M;
	return 0;
}

为什么最优解在dp[i][i+n-1]的位置呢? 这么理解:dp[i][j]为(i,j)的最有解,即每次把最优解保存在区间最右端。

比如:

   i=1 :  dp[1][n-1]为区间(1,n-1)的最优解
   i=2 :  dp[2][n+1]为区间(2,n+1)的最优解(此题为环形转链,总长度2n)

2021.9.24 线性DP:

https://www.luogu.com.cn/problem/P1282
蓝题果然更难,不看题解思路完全跑偏了。

转移思路:
要满足前i张牌的第一行点数和为j,其翻牌次数应当为 :

【已保存(计算或初始化得到)的前i张牌 次数】与【前i-1张牌 次数 加上 新加入的一张牌(分类讨论,不翻牌 +0,翻牌+1)】

可得:

f[i][j] = min(f[i][j], f[i - 1][j -> a[i]] + 0);

f[i][j] = min(f[i][j], f[i - 1][j - b[i]] + 1);

确定转移方程后就很容易得到边界了,见代码注释。

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

const int N = 1000;
const int INF = 1e9;
int a[N + 10], b[N + 10], f[N + 10][6 * N + 10];
//首先通过dp得到【前i个骰子第一行可能的点数总和】所需的翻牌次数,最后用枚举更新的方法比较差值,得到最小翻牌次数
int main()
{
	int n;
	scanf_s("%d", &n);
	int s = 0;
	for (int i = 1; i <= n; i++) {
		scanf_s("%d%d", &a[i], &b[i]);
		s += a[i] + b[i];//sum
	}
	for (int i = 1; i <= n; i++)
		for (int j = 0; j <= 6 * n; j++) f[i][j] = INF;
	//边界,若第一行和为a则说明为交换,反之已交换
	f[1][a[1]] = 0;
	f[1][b[1]] = 1;
	
	//屏幕输出的是用于理解用的辅助信息,提交时应当删去
	for (int i = 2; i <= n; i++)   //DP,解释如上
		//第一行最大总和为6*n
		for (int j = 0; j <= 6 * n; j++) {
			if (j - a[i] >= 0)
			{
				f[i][j] = min(f[i][j], f[i - 1][j - a[i]]);
				cout << i << "	" << j << "	" << f[i][j] << endl;
			}
			else
				cout << "no a!" << endl;
			if (j - b[i] >= 0)
			{
				f[i][j] = min(f[i][j], f[i - 1][j - b[i]] + 1);
				cout << i << "	" << j << "	" << f[i][j] << endl;
			}
			else
				cout << "no b!" << endl;
		}
	int minD = INF, minT = INF;  //minD是最小差值,minT是最小交换次数
	for (int i = 0; i <= s; i++)
		if (f[n][i] != INF) {//此前得到的【前n个骰子点数和为i】的翻牌次数存在
			if (abs(i - (s - i)) < minD) {//算差值
				minD = abs(i - (s - i)); minT = f[n][i];
			}
			else if (abs(i - (s - i)) == minD) 
				minT = min(minT, f[n][i]);//若与先前得到的最小差值相等,则比较翻牌次数
		}
	printf("%d", minT);
	return 0;
}

2021.9.27 多维DP:

https://www.luogu.com.cn/problem/P1006
使用多维数组解题,此前没有练习过的方法。
转移:

	dp[i][j][k][l]=max(dp[i-1][j][k-1][l],max(dp[i-1][j][k][l-1],max(dp[i][j-1][k-1][l],dp[i][j-1][k][l-1])));
	dp[i][j][k][l]+=((i==k&&j==l)?a[i][j]:(a[i][j]+a[k][l]));

dp[i][j][k][l]代表 初始点A到(i,j) + 初始点B到(l,k) 所用点数之和(可以理解成同样的起点终点走,两个不同的路线)。从终点向前回溯,更新数据时不考虑右/下方向的点数,故没有i+1和j+1。

第一行代表抵达该两点之前的即时点数,第二行代表已抵达后与自身点数相加的更新(三目运算符用于确保路径不相交)。

具体实现:

for (int i = 1; i <= n; i++)
		for (int j = 1; j <= m; j++)
			for (int k = n; k >= 1; k--)
				for (int l = m; l >= 1; l--)
				{
					dp[i][j][k][l] = max(dp[i - 1][j][k - 1][l], max(dp[i - 1][j][k][l - 1], max(dp[i][j - 1][k - 1][l], dp[i][j - 1][k][l - 1])));
					dp[i][j][k][l] += ((i == k && j == l) ? a[i][j] : (a[i][j] + a[k][l]));
				}
	printf("%d\n", dp[n][m][n][m]);

dp[n][m][n][m]代表从两条从(1,1)点出发到达(n,m)的不同路径点数和。
好像可以优化成三维数组,以后再更新。

2021.9.28 多维DP:

https://www.luogu.com.cn/problem/P1387

第一眼看过去就想用暴力枚举,将数组每一个点作为正方形左上顶点,用边长为k的正方形去框,框内总和为k*k则成功。
第一次用暴力写DP题居然就ac了

int ans = 1;//若都不为k*k,则正方形边长为1自己(前提是测试数据不给空数组)
for (int k = 2; k <= min(n, m); k++)
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= m; j++)
			{
				for (int x = i; x <= i + k-1; x++)
					for (int y = j; y <= j + k-1; y++)
					{
						if (a[x][y] == 0)//(不知道能不能叫剪枝)遇到0直接跳过,不加会TLE
							break;
						sum += a[x][y];
						if (sum == k * k)
						{
							ans = k; sum = 0;
						}
					}
				sum = 0;
			}

DP思路:从左上往右下,令DP[i][j]保存 以点(i,j)为右下顶点的最大正方形 的边长。
重点是找到规律

if (a[i][j]==1)
	DP[i][j]=min(min(DP[i][j-1],DP[i-1][j]),DP[i-1][j-1])+1;
	//+1代表

截图来自https://www.luogu.com.cn/blog/user23035/solution-p1387

https://www.luogu.com.cn/problem/P1417
结合了排序的01背包问题。
看题解之前完全没思路,参考后发现需要列出不等式,化简后得到用于排序的条件判别式:

a[i]−b[i]∗(t+c[i])+a[j]−b[j]∗(t+c[i]+c[j]) //i先做 > a[j]-b[j]∗(t+c[j])+a[i]-b[i]∗(t+c[i]+c[j]) //j先做
化简后:b[j]∗c[i] < b[i]∗c[j]

在这里也列一下01背包的一维数组实现模板,不是太熟练。

//C为容量,W为重量,V为价值
 for(int i=0;i<n;i++)
         {
             for(int j=C;j>=maze[i].W;j--)
                 dp[j] = max(dp[j-maze[i].W]+maze[i].V,dp[j]);
         }
         printf("%d",dp[C]);

本题AC代码:

long long tim, n;
long long a[100001], ans = 0;
struct food
{
	long long a, b, c;
};

bool cmp_worth(food a, food b)
{
	return a.c * b.b < a.b * b.c;
}


int main()
{

	cin >> tim >> n;
	food food[100];
	for (long long i = 1; i <= n; i++) cin >> food[i].a;
	for (long long i = 1; i <= n; i++) cin >> food[i].b;
	for (long long i = 1; i <= n; i++) cin >> food[i].c;

	sort(food + 1, food + 1 + n, cmp_worth);
	//试图新开一个数组保存每个点的worth,但运算结果不正确,遂更换方式。
	//for (long long i = 1; i <= n; i++)
	//{
	//	t[i] = t[i - 1] + food[i].c;
	//	food[i].worth = food[i].a - t[i] * food[i].b;
	//}
	//01背包模板
	for (long long i = 1; i <= n; i++)
		for (long long j = tim; j >= food[i].c; j--)
		{//j代表背包余量,此题中指代当前时间点
			a[j] = max(a[j], a[j - food[i].c] + food[i].a-food[i].b*j);
			ans = max(ans, a[j]);
		}
	cout << ans;
	return 0;
}

2021.9.29 多维DP:

https://www.luogu.com.cn/problem/P1855
常规的二维背包问题,但是由于此前没有接触过,思路走了弯路。

	for (int i = 1; i <= n; i++) 
		for (int j = m; j >= m1[i]; j--) 
			for (int k = t; k >= t1[i]; k--) 
				f[j][k] = max(f[j][k], f[j - m1[i]][k - t1[i]] + 1);
	cout << f[m][t];
}

相当于对每个i多进行了m-m1[i]次一维背包的计算,得到每一种情况的状态,最终得到在m、t下的最优解。

https://www.luogu.com.cn/problem/P1736
看题解就好,源代码和题解的差不多就不放了。

常规解法,用三维数组解(二维+一维标志方向)
二维数组解法,比较难直接想到

2021.10.14 常见数据结构:

国庆摸完后搞了几天安卓开发,今天开始继续。因为刷题的时候打算是先把课内有涉猎到的算法常规题大概先过一遍,现在就回头开一些数据结构的实现和应用了。

https://www.luogu.com.cn/problem/P1996
大一的时候不太会写的约瑟夫环。当时应该是用模拟的方法写的,今天模拟了一遍以后复习了一下队列。
一开始想的是环形队列,但是好像复杂化了。以下代码是用STL的queue实现的。

#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;

int main()
{
	int n, m;
	int cnt = 1;
	queue<int> q;
	cin >> n >> m;
	for (int i = 1; i <= n; i++)	q.push(i);
	while (!q.empty()) 
		if (cnt == m)
		{
			cout << q.front() << " ";
			q.pop();
			cnt = 1;
		}
		else
		{
			cnt++;
			q.push(q.front());
			q.pop();
		}
	return 0;
}

手写实现:https://www.cnblogs.com/etangyushan/p/11194093.html

2021.11.16 常见数据结构:

回学校以后一堆实验考试什么的居然折腾了快一个月。
练了一下树的题目,相当一部分题目都并没有真正建树,更像是模拟。

——————————————————————————
2021.11.17
重新开始按题单做

——————————————————————————

2021.11.17 排序

没怎么见过的快排写法,也记录一下:

int quicksort(int left,int right)
{//将数组a的第left到right个元素进行划分
	int mid=a[left];
	while (left<right) 
	{//采用快排策略
		while (left<right&&mid<=a[right])
			right--;
		a[left] = a[right];
		while (left<right&&a[left]<=mid)
			left++;
		a[right] = a[left];
	}
	a[left]=mid;
	return left;
	//返回的left实为flag元素的位置
}

tips:cin、cout作为输入输出流经过了缓冲区,效率上略慢于scanf、printf

2021.11.18 排序、暴力

做完了排序题单内的题,开始做暴力。复习了一下DFS。
被舍友抓来做P1922,没写出来。

2021.12.4

一直按着题单做就不记录题目了。
关于二分细节的讨论:https://www.cnblogs.com/kyoner/p/11080078.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值