算法刷题训练营(一)

算法刷题训练营

一个技巧,一般情况下,看题目所给的数据量。一般情况下,c++的单核处理效率在10的八次方左右。如果给的数据量是10的6次方,那么你给一个n平方的算法,是肯定过不了的。
在这里插入图片描述

一.绳子覆盖数组

题目:
给定一个有序数组arr,从左到右依次表示X轴上从左往右点的位置。
给定一个正整数K,返回如果有一根长度为K的绳子,最多能盖住几个点。
绳子的边缘点碰到X轴上的点,也算盖住。

比如,数组[1,3],此时绳子长为2,能盖得住。可以认为是左闭右闭(一般情况下应该是左闭右开)。

可以暴力
小贪心思想,绳子一端要去压一个。
可以用首去压,也可以用尾去压。
在这里插入图片描述
每次都这么干,其实就是来到一个点的时候,左边有序的部分,来个二分就行了。
为什么要用二分?
在这里插入图片描述
二分可以找左边/右边,大于或小于某数的第一个值,
上图中就要找左边大于等于98的第一个位置
时间复杂度为nlogn。

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

int nearster(vector<int>& arr, int R, int value);
//计算两数相差时,如计算日期a到日期b,如果这两天都算进去,则n=b-a+1;
//如果只算了一天,则n=b-a
//如果两天都不算,则n=b-a-1
int maxPoint(vector<int>& arr, int L)
{
	int ans  = 1;
	int n = arr.size();

	for (int i = 0; i < n; i++)
	{
		int nearst = nearster(arr,i,arr[i]-L);
		ans = max(ans, i - nearst + 1);//这里两个都包括,所以说要+1
	}
	return ans;
}
//关于相减+1的问题,要看两端的情况,是不是都包括,还是一段包括一段不包括。
//基础班前几节,二分法找到大于等于某个数的第一个数
int nearster(vector<int>& arr, int R, int value)
{
	int L = 0;
	int index = R;
	while (L <= R)
	{
		int mid = L + ((R - L) >> 1);
		if (arr[mid] >= value)
		{
			index = mid;
			R = mid - 1;
		}
		else
		{
			L = mid + 1;
		}
	}
	return index;
}

但这不是最优解。
最优解是窗口法,绳子覆盖的模型,就可以看成滑动窗口。
在这里插入图片描述
窗口,每到左边界的时候,都去看右边界能不能扩进来。同时记录此时里面覆盖的数字个数。

//滑动窗口来记录
int maxPoint2(vector<int>& arr, int L)
{
	int left = 0;
	int right = 0;
	int n = arr.size();
	int maxa = 0;
	
	while (left < n)
	{//左端点固定,看右端点
		while (right < n && arr[right] - arr[left] <= L)
		{
			right++;
		}
		maxa = max(maxa, right - left);//这里右点不包括,所以说是单头包括、
		left++;
	}
	return maxa;
}

时间复杂度on,因为左边界右边界都不回退。

二,括号有效配对

在这里插入图片描述

问题1.
这个题可以用栈,左括号进栈,遇到右括号进去右括号和左括号出栈。等进栈完毕后,此时栈里还有元素,就false。栈顶元素为)也直接为false。
但是左神说可以不用栈,一个变量就行了。
但要是进行一个变种的话,就会很难做了,因为还存在不配对的状况。
所以还是要掌握用辅助栈的方法。

辅助栈法的几个要点:
1.如果为奇数,则直接返回false
2.把对应关系存入哈希表,直接初始化一个哈希表,键是右括号,值是左括号
3.对于每个字符,在哈希表的键中如果能找到,则意味着是右括号,此时如果栈空或者栈顶元素 不是配对的元素时,返回false。要不然直接栈顶元素出栈。
如果哈希表的键中没找到,则代表是左括号,直接入栈。

到最后看栈是不是空的。

bool isValidByStack(string s)
{
	int n = s.size();
	if (n % 2 == 1)
	{
		return false;
	}
	unordered_map<char, char> mp =
	{
		{')','('},
		{']','['},
		{'}','{'}
	};
	stack<char> zhan;
	for (auto c : s)
	{
		if (mp.count(c))
		{
			while (zhan.empty() || zhan.top() != mp[c])
			{
				return false;
			}
			zhan.pop();
		}
		else
		{
			zhan.push(c);
		}
	}
	return zhan.empty();
}

从左往右遍历,遇到左括号,count++,遇到右括号,count–;
在遍历图中,有以下要注意的点 。
count<0时,直接返回false。
遍历完后,count是否为0;如果遍历结束不为0.那么那么直接false;

#include<iostream>
using namespace std;

bool valid(string s)
{
	int count = 0;
	int n = s.size();

	for (int i = 0; i < n; i++)
	{
		count += s[i] == ')' ? -1 : 1;
		if (count == -1)
		{
			return false;
		}
	}
	return count == 0 ? true : false;
}

问题2.

如果一个括号字符串无效,请返回至少填几个字符能让其整体有效。

在上一问的基础上改这道题就可以了。
准备两个变量,count和need。初始时两个值都为0;
一开始遍历,遇到左括号,count++,遇到右括号,count–.当某一个时刻,count变成-1了,说明右括号比左括号多一个,所以need++。并让count恢复成0;
遍历到最后,need的值就是遍历过程中需要填多少个左括号,结束时,如果count为正数,则用need把count的值加上,表示整体要添加上多少的右括号。
这样一定是最少的。

int isValid(string s)
{
	int n = s.size();
	int count = 0;
	int need = 0;
	for (int i = 0; i < n; i++)
	{
		count += s[i] == ')' ? -1 : 1;
		if (count == -1)
		{
			need -= 1;
			count = 0;
		}
	}

	return need+count;
}
三、最长的括号有效子串的长度

在这里插入图片描述
这是一个动态规划题。
这道题不能用滑动窗口,因为有可能无效的变成有效了,简单的说就是没有单调性。
当看到子串和子数组的时候,去想开头和结尾的时候答案是啥,
开头的情况下怎么怎么样,结尾的情况下怎么怎么样,这道题就解了一半了。

在这里插入图片描述

如果来到i位置的时候,能求出以i位置结尾时的答案,那么我们就可以求出以0位置结尾的答案,以1位置结尾的答案,以2位置结尾的答案,,,,,,
枚举每个位置时,以该位置结尾的答案,最终答案必在这些答案中。

在这里插入图片描述

这种本质上是想得到动态规划。通过dp数组中,i-3,i-2,i-1位置上的答案,看能不能帮助推出i位置上的答案。从而改成动态规划。

dp数组中,如果i位置上是左括号,那么dp数组内直接存成0就行。因为不会有有效的括号是以左括号结尾。
dp[0]一定是0,因为不管是有效的括号一定要是两个。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

现在在填dp[i],如果i位置是右括号,可以看dp[i-1]位置上的数,假设是4,那么就要看i-5位置上的数。当这个数是左括号的时候,则此时是有效的,dp[i]至少为dp[i-1]+2,此时再往前看一个,dp[i-6]的值,注意不要越界;如果是右括号,则该位置直接是0;

class Solution {
public:
    int longestValidParentheses(string s) 
    {
        int n = s.size();
        if(n<2)return 0;
        vector<int> dp(n,0);
        int pre =0;
        int ans =0;
        //dp[0]=0;
        for(int i = 1;i<n;i++)
        {
           /* if(s[i]=='(')
            {这里直接在初始化dp表的时候就置0了
                return 0;
            }*/
            if(s[i]==')')
            {
                pre = i-dp[i-1]-1;
                if(pre>=0&&s[pre]=='(')
                {
                    dp[i] = dp[i-1]+2+(pre>0?dp[pre-1]:0);
                }
            }
            ans = max(ans,dp[i]);

        }
        return ans;
    }
};
四、由括号组成的字符串最大嵌套几层

下面这里是嵌套两层,因为里面的两个是并列的。

下面这里是嵌套3层。
在这里插入图片描述
最大嵌套2层
在这里插入图片描述
怎么做呢?
很简单,遇到左括号count++,遇到右括号count–,count到达的最大值,就是最大嵌套层数。

int maxNum(string s)
{
	int n = s.size();
	int count  =0;
	int ans =0;
	for(int i=0;i<n;i++)
	{	
		count+= s[i]==')'?-1:1;
		ans =max(ans,count);
	}
	return ans;
}
五、涂染颜色

在这里插入图片描述这题啥意思?
一定要做到,左边是R,右边是G。也可以全是R,可以全是G,但只要是有R有G,就要是左R右G。
给一个随机的字符串,问最少多少次可以左R右G
RG的数量是可以变得。
在这里插入图片描述
可以枚举每一个分界线。得到左R右G的涂抹数,所有答案当中最少的那个,就是最终的答案。在这里插入图片描述
需要预处理数组的方式去做
先弄个辅助数组,保存0-i位置上有几个G和i到n-1位置上有几个R
在这里插入图片描述

在枚举每一个分界线的情况下,用辅助数组来加速查询。
其实也可以不用L数组,因为结算的时候是从左往右结算的,只要用一个变量记录下来就好。
最后其实也不需要R数组,只要从左往右遍历的时候,R–,L++就可以了。

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

int minPaint(string s)
{
	int n = s.size();
	if (n < 2)return 0;
	int rAll = 0;
	for (int i = 0; i < n; i++)
	{
		rAll += s[i] == 'R' ? 1 : 0;//计算总共多少个R
	}
	int ans = rAll;//如果数组所有的范围,都是右侧范围,都变成G(把所有R都变成G的话,需要多少次)
	int left = 0;
	for (int i = 0; i < n ; i++)//这里i是指边界,边界从0开始往右移动
	{
		left += s[i] == 'G' ? 1 : 0;//随着边界的移动,左边需要改变的数量也在不断变化
		rAll -= s[i] == 'R' ? 1 : 0;//随着边界的移动,右边需要改变的数量也在不断变化
		ans = min(ans, left + rAll);
	}
	//0 .. n-1左全部,右无
	//ans = min(ans, left + (s[n - 1] == 'G' ? 1 : 0));
	return ans;
}

int main()
{
	string a = "GGRRR";
	cout << minPaint(a) << endl;

	system("pause");
	return 0;
}
六、矩阵

在这里插入图片描述
里面怎么样不去管,找边框,求边框最大的正方形边长是多长。
如果一个矩阵是n*n的,长方形的数量是n的四次方,点两个点。正方形的数量是n的三次方,按边长来进行枚举。
正方形的三个循环
在这里插入图片描述
但是最后还要去验证,验证每个边是不是1,这样又一个n,所以是n的四次方。
用预处理来进行优化。
弄两个辅助数组。r[][]和d[][],分别代表右边和下边有几个连续的1。
在这里插入图片描述
两个辅助数组一开,就可以不用遍历,直接查询就可以得到是否符合该情况。
辅助数组画图进行推画。

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

void setBoardMap(vector<vector<int>>& board, vector<vector<int>>& right, vector<vector<int>>& down);
bool hasSizeOfBoard(int size, vector<vector<int>>& right, vector<vector<int>>& down);
int getMaxSize(vector<vector<int>>& board)
{
	int m = board.size();
	int n = board[0].size();

	vector<vector<int>> right(m, vector<int>(n));
	vector<vector<int>> down(m, vector<int>(n));

	setBoardMap(board, right, down);

	for (int size = min(m, n); size > 0; size--)
	{
		if (hasSizeOfBoard(size, right, down))
		{
			return size;
		}
	}
	return 0;
}
void setBoardMap(vector<vector<int>>& board,vector<vector<int>>& right,vector<vector<int>>& down)
{
	int m = board.size();
	int n = board[0].size();

	if (board[m - 1][n - 1] == 1)
	{
		right[m - 1][n - 1] = 1;
		down[m - 1][n - 1] = 1;
	}
	for (int i = m - 2; i >= 0; i--)
	{
		if (board[i][n - 1] == 1)
		{
			right[i][n - 1] = 1;
			down[i][n - 1] = down[i + 1][n - 1] + 1;
		}
	}
	for (int i = n - 2; i != -1; i--) {
		if (board[m - 1][i] == 1) {
			right[m - 1][i] = right[m - 1][i + 1] + 1;
			down[m - 1][i] = 1;
		}
	}
	for (int i = m - 2; i != -1; i--) {
		for (int j = n - 2; j != -1; j--) {
			if (board[i][j] == 1) {
				right[i][j] = right[i][j + 1] + 1;
				down[i][j] = down[i + 1][j] + 1;
			}
		}
	}
}
bool hasSizeOfBoard(int size, vector<vector<int>>& right, vector<vector<int>>& down)
{
	for (int i = 0; i < right.size() - size + 1; i++)
	{
		for (int j = 0; j < right[0].size(); j++)
		{
			if (right[i][j] >= size && 
				down[i][j] >= size && 
				right[i + size - 1][j] >= size
				&& down[i][j + size - 1] >= size) 
			{
				return true;
			}
		}
	}
	return false;
}


int main()
{

	system("pause");
	return 0;
}

六.构造数组

题目六:给定一个正整数M,请构造出一个长度为M的数组arr,要求对任意的i,j,k三个位置,如果i<j<k,都有arr[i]+arr[k]!=2arr[j],返回构造出的arr。

在这里插入图片描述
假设1,5,3这三个数的长度做到了,那么怎么还能做到呢?1 作为第1的奇数12-1=1,5对应第5的奇数25-1=9.,3对应的奇数,3*2-1=5;
所以下面的【1,9,5】也是可以的。
在这里插入图片描述
任意拿出三个位置,左边位置加右边位置不等于中间位置的二倍。这个中间不一定是中点。
思路:这是一个比较难的贪心,假设a+b !=2c,则2a-1+2b-1!=2(c-1)

在这里插入图片描述
对于偶数也是如此:
在这里插入图片描述

不管是变成奇数,还是偶数,都能得到同样的有效的。
我们从1,5,3构造一个长度为6的数组来。
在这里插入图片描述
左边奇数,右边偶数并起来即可。可得到原来数组的两倍的数组。
当数组长度为奇数时,可以在得到大于该奇数的偶数数组时,进行相应的缩减即可。但是可能会需要递归。
在这里插入图片描述

在这里插入图片描述

首先,长度为1时,违规情况,所以说可以。

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

// 生成长度为size的达标数组
// 达标:对于任意的 i<k<j,满足   [i] + [j]  != [k] * 2
vector<int> makeNo(int size)
{
	if (size == 1)return vector<int>(1,1);
	// size
	// 一半长达标来
	// 7   :   4
	// 8   :   4
	// [4个奇数] [3个偶]
	int halfSize = (size + 1) / 2;
	vector<int> base = makeNo(halfSize);
	// base -> 等长奇数达标来
	// base -> 等长偶数达标来
	vector<int> ans(size);
	int index = 0;
	for (; index < halfSize; index++)
	{
		ans[index] = base[index] * 2 - 1;
	}
	for (int i = 0; index < size; index++, i++)
	{
		ans[index] = base[i] * 2;
	}
	return ans;
}

bool isValid(vector<int>& arr)
{
	int n = arr.size();
	for (int i = 0; i < n; i++)
	{
		for (int k = i + 1; k < n; k++)
		{
			for (int j = k + 1; j < n; j++)
			{
				if (arr[i] + arr[j] == 2 * arr[k])
				{
					return false;
				}
			}
		}
	}
	return true;
}


int main()
{
	const int N = 100;
	vector<int> ans = makeNo(N);
	
	cout << isValid(ans) << endl;

	system("pause");
	return 0;
}

题目七:最大路径和

题目七:
给定一个二叉树的头节点head,路径的规定有以下三种不同的规定。
1.路径必须是头结点出发,到叶节点为止,返回最大路径和。
2.路径可以从任何节点出发,但必须下走到达任何节点,返回最大路径和。
3.路径可以从任何节点出发,到任何节点,返回最大路径和。

注意,到叶节点为止,这里叶节点要明确是左节点为nullptr,右节点为nullptr。

1.从头节点出发,到叶节点为止
在这里插入图片描述
这种情况下,这个只有4种可能。
二叉树的递归套路。
设定一个全局maxSum。全局maxsum是通用的,但是只有到了叶节点的时候才能进行比较,更新。
给一个pre记录当前(不含当前结点)的已经积累了的路径和。

struct TreeNode
{
	TreeNode* left;
	TreeNode* right;
	int val;

	TreeNode() :left(nullptr), right(nullptr),val(0) {};
	TreeNode(int v) :left(nullptr), right(nullptr), val(v) {};
	TreeNode(TreeNode* l, TreeNode* r, int v) :left(l), right(r), val(v) {}
};
int maxSum = INT_MIN;
void process(TreeNode* head, int pre)
{
	if (head->left == nullptr && head->right == nullptr)
	{
		maxSum = max(maxSum, pre + head->val);
	}
	if (head->left == nullptr)
	{
		process(head->left, pre + head->val);
	}
	if (head->right != nullptr)
	{
		process(head->right, pre + head->val);
	}
}
int maxPath(TreeNode* head)
{
	maxSum = INT_MIN;
	process(head, 0);
	return maxSum;
}

用二叉树的递归套路如下:

// head为头的整棵树上,最大路径和是多少,返回。
	// 路径要求,一定从head出发,到叶节点,算做一个路径
int process2(TreeNode* head) {
	if (head->left == nullptr && head->right == nullptr) 
	{
		return head->val;
	}
	int next = INT_MIN;
	if (head->left != nullptr) {//注意:左边不等于空,才能调用左边
		next = process2(head->left);
	}
	if (head->right != nullptr) {//右边不等于空,才能调用右边和之前的值相比较
		next = max(next, process2(head->right));
	}
	return head->val + next;
}

2.从任何节点出发,下走到任何节点(注意,必须是下走)

任何节点出发,因此我们要分类,分类的依据是,与x相关和与x无关。
1.与x无关的时候,1.左树上的整体最大路径和2.右树上的整体最大路径和。两者中取最大值
2.与x有关的时候,3.x自己(左树,右树全为负的)4.x往左走,5.x往右走。

用二叉树的递归套路,写结构体。
5种可能当中取最大的。

#include<iostream>
#include<vector>

using namespace std;

struct TreeNode
{
	TreeNode* left;
	TreeNode* right;
	int val;

	TreeNode() :left(nullptr), right(nullptr), val(0) {};
	TreeNode(int v) :left(nullptr), right(nullptr), val(v) {};
	TreeNode(TreeNode* l, TreeNode* r, int v) :left(l), right(r), val(v) {}
};


struct Info 
{
	int allTreeMaxSum;//该树的最大路径和
	int fromHeadMaxSum;//从头结点开始的最大路径和

	Info(int all, int from) :allTreeMaxSum(all), fromHeadMaxSum(from) {};
};

Info process(TreeNode* head)
{
	if (head == nullptr)
	{
		return Info(0, 0);
	}

	Info leftInfo = process(head->left);
	Info rightInfo = process(head->right);

	int p1 = INT_MIN;
	int p2 = INT_MIN;
	if (head->left)
	{
		p1 = leftInfo.allTreeMaxSum;
	}
	if (head->right)
	{
		p2 = rightInfo.allTreeMaxSum;
	}
	int p3 = head->val;
	int p4 = INT_MIN;
	if (head->left)
	{
		p4 = head->val + leftInfo.fromHeadMaxSum;
	}
	int p5 = INT_MIN;
	if (head->right)
	{
		p5 = head->val + rightInfo.fromHeadMaxSum;
	}
	int allTreeMaxSum = max(p1, max(max(p2, p5), max(p3, p4)));
	int fromHeadMaxSum = max(max(p3, p4), p5);
	return Info(allTreeMaxSum, fromHeadMaxSum);
}

int maxSum(TreeNode* head)
{
	if (head == nullptr)return 0;
	return process(head).allTreeMaxSum;
}

3.任何节点出发,到任何节点

X无关的时候,1.左树上的整体最大路径和 2, 右树上的整体最大路径和
X有关的时候 3.x自己 4. x往左走 5.x往右走 6.既往左,又往右

#include<iostream>
#include<vector>

using namespace std;

struct TreeNode
{
	TreeNode* left;
	TreeNode* right;
	int val;

	TreeNode() :left(nullptr), right(nullptr), val(0) {};
	TreeNode(int v) :left(nullptr), right(nullptr), val(v) {};
	TreeNode(TreeNode* l, TreeNode* r, int v) :left(l), right(r), val(v) {}
};

struct Info
{
	int allTreeMaxSum;//该树的最大路径和
	int fromHeadMaxSum;//从头结点开始的最大路径和

	Info(int all, int from) :allTreeMaxSum(all), fromHeadMaxSum(from) {};
};

Info process(TreeNode* head)
{
	if (head == nullptr)return Info(0, 0);
	Info leftInfo = process(head->left);
	Info rightInfo = process(head->right);

	int p1 = INT_MIN;
	if (head->left)
	{
		p1 = leftInfo.allTreeMaxSum;
	}

	int p2 = INT_MIN;
	if (head->right)
	{
		p2 = rightInfo.allTreeMaxSum;
	}
	int p3 = head->val;

	int p4 = INT_MIN;
	int p5 = INT_MIN;
	int p6 = INT_MIN;

	if (head->left)
	{
		p4 = head->val + leftInfo.fromHeadMaxSum;
	}
	if (head->right)
	{
		p5 = head->val + rightInfo.fromHeadMaxSum;
	}
	if (head->left && head->right)
	{
		p6 = head->val + leftInfo.fromHeadMaxSum + rightInfo.fromHeadMaxSum;
	}
	int allTreeMaxSum = max(max(max(p1, p2), p3), max(max(p4, p5), p6));
	int fromHeadMaxSum = max(max(p3, p4), p5);//从头出发这件事情,不能把6算进去,6是在中间,只是包含头,但是不能算作从头出发。
	return Info(allTreeMaxSum, fromHeadMaxSum);
}

4.大帅的题:路径从任何节点出发,以叶节点为止

#include<iostream>
#include<vector>

using namespace std;

struct TreeNode
{
	TreeNode* left;
	TreeNode* right;
	int val;

	TreeNode() :left(nullptr), right(nullptr), val(0) {};
	TreeNode(int v) :left(nullptr), right(nullptr), val(v) {};
	TreeNode(TreeNode* l, TreeNode* r, int v) :left(l), right(r), val(v) {}
};

int maxa = INT_MIN;
int bigshuai(TreeNode* head)
{
	if (head->left == nullptr && head->right == nullptr) 
	{
		maxa = max(maxa, head->val);
	}
	int nextMax = 0;
	if (head->left != nullptr)
	{
		nextMax = bigshuai(head->left);
	}
	if (head->right != nullptr)
	{
		nextMax = bigshuai(head->right);
	}
	int ans = head->val + nextMax;
	maxa = max(maxa, ans);
	return ans;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值