第一节课:滑动窗口、打表和预处理数据


题目一

在这里插入图片描述
方法一:每个点作为绳子右侧端点的情况下能覆盖几个点,遍历每个点,结果一定在其中。基础班有个技巧,找到大于等于某个数最左的位置(二分)。时间复杂度:一共检查了N个点,每次做二分,所有时间复杂度为O(N*logN)。
在这里插入图片描述
方法二:滑动窗口。L、R两个指针,R向右移动,但L、R两指针对应元素的差不要超过绳子的长度;当R不能再移动时,L向右移动一个位置;然后重复上述操作。时间复杂度:O(N)
方法一是用二分的方式确定一个七点,而方法二是用滑动窗口单调不递减的方式确定一个终点,构造了一个左边界和右边界都不需要回退的模型
代码实现:

//方法一
int getNearestPos(vector<int>& arr, int i, int num) {
	int l = 0;
	int r = i;
	while (l <= r) {
		//mid = r - ((r - l) >> 1);
		int mid = l + ((r - l) >> 1);
		if (arr[mid] > num) {
			r = mid - 1;
			i = mid;
		}
		else if (arr[mid] < num) {
			l = mid + 1;
		}
		else {
			return mid;
		}
	}
	return i;
}

int maxCoverPoint01(vector<int>& arr, int len) {
	int res = 0;
	for (int i = 0; i < arr.size(); i++) {
		int nearestIndex = getNearestPos(arr, i, arr[i] - len);
		res = max(res, i - nearestIndex + 1);
	}
	return res;
}

//方法二
int maxCoverPoint02(vector<int>& arr, int len) {
	int l = 0;
	int r = 0;
	int res = 0;
	while (r < arr.size()) {
		if (arr[r] - arr[l] < len) {
			r++;
		}
		else if (arr[r] - arr[l] > len) {
			res = max(res, r - l);
			l++;
			r++;
		}
		else {
			res = max(res, r - l + 1);
			l++;
			r++;
		}
	}
	return res;
}

题目二

在这里插入图片描述
方法一:尽量使用装8个的袋子,剩下的再尝试用装6个的袋子,如果能装下则返回,如果装不下,则让装8个的袋子依次递减
在这里插入图片描述
优化:当用装8个的袋子装剩下的苹果数超过24时,后面的尝试都不成立,因为,超过24个苹果可以用三个8袋子,二不必用4个6袋子
方法二:打表:输入是一个正数,输出也是一个正数,先写出一个好想的的代码,然后分析输出的规律,根据规律对代码进行优化,对应的数学规律不管。大概能解决4成的题目。

//方法一
int minBags(int n) {
	if (n < 0 || n % 2 == 1) {
		return -1;
	}
	for (int i = n / 8; i >= 0; i--) {
		if ((n - i * 8) % 6 == 0) {
			return i + (n - i * 8) / 6;
		}
		if (n - i * 8 > 24) {
			break;
		}
	}
	return -1;
}

//方法二
int minBags02(int n) {
	if (n % 2 == 1) {
		return -1;
	}
	if (n < 18) {
		return n == 0 ? 0 : (n == 6 || n == 8 ? 1 : (n == 12 || n == 14 || n == 16 ? 2 : -1));
	}
	return n % 8 == 0 ? n / 8 : n / 8 + 1;
}

题目三

在这里插入图片描述
方法一:暴力递归。确定basecase;先手依次尝试吃4的幂次方份草,只要后序结果有一种能让先手获胜,即先手获胜(因为他们都是绝顶聪明的);另外,先手在进入递归函数之后变成了后手;最后还需注意整数溢出的情况
方法二:打表。根据方法一的解决,找规律,发现总是“后先后先先”进行循环,直接编写代码,不去管具体的数学原理。
代码实现:

//方法一:
string f(int n) {
	// 0 1 2 3 4
	//后先后先先
	//n<5时只有一种选择,确定为basecase
	if (n < 5) {
		return n == 0 || n == 2 ? "后手" : "先手";
	}
	int base = 1;
	while(base<=n) {
		//当前一共n份草,先手吃掉base份,n-base留给后手的草
		//木过程的先手,在子过程中是后手
		if (f(n - base)=="后手") {
			return "先手";
		}
		if (base > n / 4) {//防止溢出
			break;
		}
		base *= 4;
	}
	return "后手";
}
//方法二:打表
string f2(int n) {
	
	if (n % 5 == 0 || n % 5 == 2) {
		return "后手";
	}
	return "先手";
}

题目四

在这里插入图片描述
方法一:暴力尝试。遍历整个字符串,将遍历到的位置及之后全部变成G,之前全部变成R,统计涂色次数的最小值。
方法二。数据预处理:研究代码,发现那一块的查询是频繁的,想办法用低代价的方式生成某个结构,后序就不需要遍历,直接从辅助结构中那答案,加速整个过程。方法一每次遍历到一个位置,还需要前后扫描有多少R和G,变成了一个双层循环,时间复杂度为O(N^2)。可以生成两个数组,分别记录当前位置及之前有多少G(前缀和),当前位置及之后有多少R(后缀和);对方法一中每次遍历到的位置需要的数据,直接从数组中取出即可。
在这里插入图片描述

//方法一:O(N^2)
int f(string& s) {
	int res = INT_MAX;
	for (int i = 0; i < s.length() + 1; i++) {
		int count = 0;
		for (int j = 0; j < i - 1; j++) {
			if (s[j] == 'G') {
				count++;
			}
		}
		for (int j = i; j < s.length(); j++) {
			if (s[j] == 'R') {
				count++;
			}
		}
		res = min(res, count);
	}
	return res;
}

//方法二:O(N)
int f2(string& s) {
	int N = s.length();
	vector<int>arr(N);
	arr[0] = s[0] == 'G' ? 1 : 0;
	for (int i = 1; i < N; i++) {
		//R:0  G:1,当前位置及之前有多少个1
		arr[i] = s[i] == 'G' ? arr[i - 1] + 1 : arr[i - 1];
	}
	int res = N - arr[N - 1];//全部变成G-->总共R的个数
	for (int i = 1; i < N; i++) {
		//当前位置及后面全部变成G:1
		//当前位置之前的1:
		int num_1 = arr[i - 1];//这么多1要全部变成0
		//当前位置及之后有多少0:
		int num_0 = N - i - (arr[N - 1] - arr[i - 1]);//这么多0要全部变成1
		res = min(res, num_1 + num_0);
	}
	return res;
}

题目五

在这里插入图片描述
方法一:N*N大小的矩阵的子矩阵(长方形)有N4规模个,均以N2分之一的概率选两个点,以这两个点为对角线画矩阵;若规定必须是正方形,则是N3规模,以N2分之一的概率选一个点,以该点为左上角枚举各个边长的正方形个数。
在这里插入图片描述
遍历矩阵的各个位置,以当前位置为正方形的左上顶点,每次向下/向右扩一个位置,判断当前正方形是否符合条件
方法二:数据预处理。用两个数组right和down分别记录原数组某一位置向右(包括当前)有多少连续的1、原数组某一位置向下(包括当前)有多少连续的1

在这里插入图片描述
代码实现:

//方法一:O(N^4)!!!!
//代码错误,不想改了。。。
int f(vector<vector<int>>& arr) {
	int N = arr.size();
	int res = 0;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			if (arr[i][j] == 1) {
				int k = 1;
				while (i + k < N && j + k < N && arr[i + k][j]==1 && arr[i][j + k]==1) {
					bool flag = true;
					for (int r = 1; r <= k; r++) {
						if (arr[r+i][j + k] != 1||arr[i+k][j+r]!=1) {
							flag = false;
							break;
						}
					}
					if (flag == false) {
						break;
					}
					k++;
				}
				res = max(res, k);
			}
		}
	}
	return res;
}

//方法二:O(N^3)
int f2(vector<vector<int>>& arr) {
	int N = arr.size();
	vector<vector<int>>right(N, vector<int>(N));
	vector<vector<int>>down(N, vector<int>(N));
	for (int i = 0; i < N; i++) {
		right[i][N - 1] = arr[i][N - 1];
		for (int j = N - 2; j >= 0; j--) {
			right[i][j] = arr[i][j] == 0 ? 0 : 1 + right[i][j + 1];
		}
	}

	for (int j = 0; j < N; j++) {
		down[N - 1][j] = arr[N - 1][j];
		for (int i = N - 2; i >= 0; i--) {
			down[i][j] = arr[i][j] == 0 ? 0 : 1 + down[i + 1][j];
		}
	}

	int res = 0;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < N; j++) {
			if (arr[i][j] == 1) {
				for (int k = 1; k + i < N && k + j < N; k++) {
					if (right[i][j] >= k + 1 && down[i][j] >= k + 1 && right[i + k][j] >= k + 1 && down[i][j + k] >= k + 1) {
						res = max(res, 1 + k);
					}
				}
			}
		}
	}
	return res;
}


题目六

在这里插入图片描述
用二进制拼:将等概率返回a-b的数字的函数加工成等慨率返回0/1的函数:如果a-b有偶数个,则可以规定返回前一半数字认为是0;返回后一半数字认为是1,如果a-b有奇数个,认为返回中间的数字重置,直到返回前半数字或者后半数字;生成c-d等价于生成0-d-c,然后再+c;假设d-c用二进制表示有n位,则用0/1生成器决定每一位是0还是1;如果d-c不是n位二进制最大的数,则每次出现n位二进制最大的数,进行重置

在这里插入图片描述
f函数以概率p返回0,以概率1-p返回1,调用两次f函数,则可能出现四种结果:00:pp、01:p(1-p)、10:(1-p)p和11:(1-p)(1-p);出现01和10的概率相等,可以将他们分别与0/1对应;如果出现00或者11,则进行重置

在这里插入图片描述
代码实现:

/随机返回1-5
int f() {
	//srand((unsigned)time(NULL));
	return rand() % 5 + 1;
}
//随机生成1-7
int g() {
	int res = 0;
	int i = 0;
	do {
		while (i < 3) {
			int rd = f();
			cout << rd << endl;
			while (rd == 3) {
				rd = f();
				cout << rd << endl;
			}
			if (rd > 3) {
				//置1
				res = (res | (1 << i));
			}
			else {
				res = (res | (0 << i));
			}
			i++;
		}
	} while (res == 7);
	return res + 1;
}


//以概率2/5生成0,以概率3/5生成1
int f2() {
	int rd = rand() % 5;
	return rd < 2 ? 0 : 1;
}
//等概率返回0/1
int g2() {
	int rd1 = f2();
	int rd2 = f2();
	while (rd1 == rd2) {
		rd1 = f2();
		rd2 = f2();
	}
	cout << rd1 << endl;
	cout << rd2 << endl;
	return rd1 > rd2 ? 0 : 1;
}

题目七

在这里插入图片描述
动态规划题目:basecase:1) 0个节点–>空树、2) 1个节点–>1种结构、3) 2个节点–>2种结构

在这里插入图片描述
一般情况:假设有N个节点,1) 左树没有节点,右树N-1个节点;2) 左树1个节点,右树N-2个节点…

在这里插入图片描述
代码实现:

//暴力递归
int numTrees(int n) {
	if (n < 0) {
		return 0;
	}
	if (n == 0 || n == 1) {
		return 1;
	}
	if (n == 2) {
		return 2;
	}
	int res = 0;
	for (int leftNum = 0; leftNum <= n - 1; leftNum++) {
		int leftWays = numTrees(leftNum);
		int rightWays = numTrees(n - 1 - leftNum);
		res = res + leftWays * rightWays;
	}
	return res;
}
//改动态规划
int numTrees02(int n) {
	if (n < 2) {
		return 1;
	}
	vector<int>dp(n + 1);
	dp[0] = 1;
	dp[1] = 1;
	dp[2] = 2;
	for (int i = 3; i <= n; i++) {
		for (int j = 0; j < i; j++) {
			dp[i] = dp[i] + dp[j] * dp[i - 1 - j];
		}
	}

	return dp[n];
}


题目八

在这里插入图片描述
如何判断括号字符串是否完整?遍历字符串,用一个变量count,遇到‘(’count++,遇到‘)’count–;1) 如果遍历过程种count有小于0的时刻,则括号字符串肯定不完整,因为如果出现这种情况,说明‘)’多,而‘)’多后面是每办法再进行匹配了;2) 遍历完之后,count必须等于0,括号字符串才是完整的
对于不完整的括号字符串,这么让他完整?借用上述过程:用一个变量count,遇到‘(’count++,遇到‘)’count–,再用一个变量存储最终的结构res;1) 如果某时刻出现count==-1,res++(count==-1说明‘)’多了,需要在前面补一个‘(’),然后让count置0;2) 如果遍历到最后count>0,说明‘(’多了,需要在后面补count个‘)’
在这里插入图片描述
代码实现:

//判断括号字符串是否完整
bool isWhole(string& s) {
	int count = 0;
	for (int i = 0; i < s.length(); i++) {
		if (s[i] == '(') {
			count++;
		}
		else {
			count--;
			if (count < 0) {
				return false;
			}
		}
	}
	return count == 0 ? true : false;
}
//最少补多少字符使之完整
int needParentheses(string& s) {
	int count = 0;
	int ans = 0;
	for (int i = 0; i < s.length(); i++) {
		if (s[i] == '(') {
			count++;
		}
		else {
			count--;
			if (count < 0) {
				ans++;
				count = 0;
			}
		}
	}
	return ans + count;
}

总结

本节课讲到三个技巧:窗口不回退模型,当发现窗口的左右边界可以不回退时,算法流程之和左右边界运动的距离有关,很多O(N)的算法是这么加工而来的。打表,当发现输入参数和输出参数都比较简单时,利用笨方法找规律改代码。数据预处理,当定出流程,发现查询很频繁,代价很大,想办法将查询变成O(1)级别的查询

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值