算法笔记(4)

题目1

小明手中有n块积木,并且小明知道每块积木的重量。现在小明希望将这些积木堆起来
要求是任意一块积木如果想堆在另一块积木上面,那么要求:

  1. 上面的积木重量不能小于下面的积木重量
  2. 上面积木的重量减去下面积木的重量不能超过x
  3. 每堆中最下面的积木没有重量要求

现在小明有一个机会,除了这n块积木,还可以获得k块任意重量的魔法积木。
小明希望将积木堆在一起,同时希望积木堆的数量越少越好,你能帮他找到最好的方案么?
输入描述:
第一行三个整数n,k,x, 1 <= n <= 200000, 0 <= X, k <= 1000000000
第二行n个整数,表示积木的重量,任意整数范围都在[1, 100000000]
样例输入:
13, 1, 38
20, 20, 80, 70, 70, 70, 420, 5, 1, 5, 1, 60, 90
输出:2
解释:
两堆分别是
1, 1, 5, 5, 20, 20, x, 60, 70, 70, 70, 80, 90
420
其中x是一个任意重量的积木,夹在20和60之间可以让积木继续往上搭

思路

贪心。先从小到大排序,从左到右遍历,当后一个数字减去当前数字不大于x时,表示后一个数字可以搭在当前数字上
第一个贪心:在从左到右遍历排序后数组搭积木的时候,如果可以一直搭(即后一个数字减去当前数字不大于x),没有必要另起一堆新积木
当后一个数字b减去当前数字a大于x时,搭不上去了,这时候需要另起一堆,记住这时候的b - a的差值s。从左到右遍历数组统计所有的s。
第二个贪心:s从小到大用魔法积木去弥补差距,因为魔法积木的数量有限,s越小,需要的魔法积木越少,而更大的s即使消耗更多的积木,所弥补的不过也就是一个裂隙,跟消耗更少的积木所获得收益一样,所以没有必要消耗更多的积木去获得同样的收益。

代码

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

int main()
{
	int n, k, x;
	cin >> n >> k >> x;
	vector<int> arr(n);
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	sort(arr.begin(), arr.end());
	int split = 1;
	vector<int> s;
	bool flag = false;
	for (int i = 1; i < n; i++)
	{
		if (arr[i] - arr[i - 1] > x)
		{
			split++;
			s.push_back(arr[i] - arr[i - 1]);
		}
	}
	if (split == 1 || x == 0 || k == 0)
	{
		flag = true;
		cout << split << endl;
	}
	sort(s.begin(), s.end());
	for (int i = 0; i < s.size(); i++)
	{
		int needs = (s[i] - 1) / x;  //(s[i] - 1) / x是计算所需魔法积木的公式,下面讲解
		if (needs <= k)
		{
			k -= needs;
			split--;
		}
		else
		{
			break;
		}
	}
	if (!flag)
	{
		cout << split << endl;
	}
	return 0;
}

整数除法向上取整(lc992)

假设要计算整数除法 a / b 向上取整的结果,那么公式就是(a + b - 1) / b;
needs = (s[i] - 1) / x 是怎么得到的?我们通过观察可以得到需要积木数量的公式为: (s[i] - x) / x 向上取整, s[i] - x就是a,x就是b,带入即可得到(s[i] - 1) / x

题目2

给定一个整数数组arr和一个数字k,找出刚好含有k种数字的arr的子数组的个数。

思路

思路1:找出含有小于等于k种数字的子数组数量s1和含有小于等于k-1种数字的子数组数量s2,s1-s2就是结果
思路2:2个滑动窗口。第一个窗口保持里面有k-1种数字,第二个窗口保持里面有k种数字。两个窗口右边界同步,从0开始同时移动(即最外面for循环中每次循环都++的i),在移动的过程中,左边界调整位置保持第一个窗口里面有k-1种数字,第二个窗口里面有k种数字。每次循环统计两个窗口左边界的差值,就是必须以i结尾时,一定有k种数的子数组数量。

代码

//思路1代码,lc超时
int getAns(vector<int> arr, int k)    //返回数组arr中,含有小于等于k种数字的子数组数量。固定尾部法,每次固定窗口右边界统计数量,即一定以i结尾,有多少含有小于等于k种数字的子数组数量
{
	if (arr.size() < 1 || k == 0)
	{
		return 0;
	}
	int L = 0;
	int ans = 0;
	unordered_map<int, int> map;
	for (int i = 0; i < arr.size(); i++)    //i是窗口右边界
	{
		if (map.count(arr[i]) == 0 || map[arr[i]] == 0)    //i位置进窗口,并调整k的值,k在这里表示还有k种数可以进窗口
		{
			k--;
		}
		map[arr[i]]++;    //调整map,包括添加没进来的数的记录,和进来过窗口的数的记录++
		while (k < 0)    //当k < 0,代表窗口内种类超过了,窗口左边界开始缩小,直到刚好k种
		{
			map[arr[L]]--;
			if (map[arr[L]] == 0)
			{
				k++;
			}
			L++;
		}
		ans += i - L + 1;    //统计个数
	}
	return ans;
}

int main()
{
	int n, k;
	cin >> n >> k;
	vector<int> arr(n);
	for (int i = 0; i < k; i++)
	{
		cin >> arr[i];
	}
	cout << getAns(arr, k) - getAns(arr, k - 1) << endl;
	return 0;
}
//第二种思路代码,lcac
int main()
{
	int n, k;
	cin >> n >> k;
	vector<int> arr(n);
	for (int i = 0; i < k; i++)
	{
		cin >> arr[i];
	}
	unordered_map<int, int> lessMap;
	unordered_map<int, int> equalMap;
	int lessKind = 0;
	int equalKind = 0;
	int lessLeft = 0;
	int equalLeft = 0;
	int ans = 0;
	for (int r = 0; r < arr.size(); r++)
	{
		if (lessMap.count(arr[r]) == 0 || lessMap[arr[r]] == 0)
		{
			lessKind++;
		}
		if (equalMap.count(arr[r]) == 0 || equalMap[arr[r]] == 0)
		{
			equalKind++;
		}
		lessMap[arr[r]]++;
		equalMap[arr[r]]++;
		while (lessKind == k)
		{
			lessMap[arr[lessLeft]]--;
			if (lessMap[arr[lessLeft]] == 0)
			{
				lessKind--;
			}
			lessLeft++;
		}
		while (equalKind > k)
		{
			equalMap[arr[equalLeft]]--;
			if (equalMap[arr[equalLeft]] == 0)
			{
				equalKind--;
			}
			equalLeft++;
		}
		ans += lessLeft - equalLeft;
	}
	cout << ans << endl;
	return 0;
}

题目3

来自微软面试
给定一个正数数组arr,长度为n,正数x和y
你的目标是让arr整体的累加和sum <= 0
你可以对数组中的数num执行以下两种操作中的一种,且每个数最多能执行一次操作:

  1. 可以选择让num变成 0,承担x的代价
  2. 可以选择让num变成 -num,承担y的代价
    返回你达到目标的最小代价
    数据规模:面试时面试官没有说数据规模

思路

我们先确定一个前提,正常情况下y的代价肯定是要比x大的,因为x只能将sum减少num,而y可以让sum减少2 * num。

我们首先讨论y > x的情况:
首先数组从大到小排序,我们先明确这样一种贪心策略:从大到小完排序的数组从左到右先是一组数字y操作(y操作可能为0次),然后一组数字x操作(x操作可能为0次),最后是不做操作。因为y代价大所以为了收益更大,应该给更大的数字用。在从大到小完排序的数组中不可能出现yxy,或者xyx这种交叉操作,因为收益会变小。基于上面的贪心思路,当y的操作次数决定时,x和不做操作的数量也可以确定,所有我们可以从左到右,依次将y操作从0个开始尝试,直到每个数字都执行y操作,期间代价最小的就是结果。因为数组从大到小排序了,前面的数变成负数,即可抵消后面一部分不做操作的数,剩下的中间的是x操作的数,那么每次看y能抵消多少个后面的数字,就可以知道x操作的次数,这样就能计算出这次的总代价。实际上不需要尝试到每个数字都采用y操作,只需要尝试到前缀和大于等于sum/2的第一个位置即可。

y <= x的情况:
首先数组从大到小排序,因为x的代价比y大或相等。而x的收益比y小,所以完全没必要采用x操作,从左到右依次增加y操作的次数,直到sum小于等于0即可。

代码

class Compare
{
public:
	bool operator()(int a, int b)
	{
		return a > b;
	}
};

int main()
{
	int n, x, y;
	cin >> n >> x >> y;
	vector<int> arr(n);
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	if (n < 1)
	{
		cout << 0 << endl;
		return 0;
	}
	sort(arr.begin(), arr.end(), Compare());
	if (x >= y)
	{
		int sum = 0;
		int cost = 0;
		for (int num : arr)
		{
			sum += num;
		}
		for (int i = 0; i < arr.size() && sum > 0; i++)
		{
			sum -= arr[i] << 1;
			cost += y;
		}
		cout << cost << endl;
	}
	else
	{
		for (int i = arr.size() - 2; i >= 0; i--)    //把arr处理成后缀和的形式,方便找到y操作能抵消的最左位置
		{
			arr[i] += arr[i + 1];
		}
		int benefits = 0;           //y操作能抵消的大小
		int left = arr.size();      //left是y操作能抵消的最左位置
		for (; left > 0; left--)    //由于y操作越来越多,能抵消的数字就越来越多,所以采用指针不回退的方式找到y操作能抵消的最左位置
		{
			if (arr[left - 1] > benefits)  //当前一个位置的后缀和大于y操作能抵消的大小,跳出循环
			{
				break;
			}
		}
		int cost = left * x;      //统计0个y操作时的代价
		for (int i = 0; i < n - 1; i++)  //0 ~ i之间的数字采用y操作
		{
			benefits += arr[i] - arr[i + 1];    //由于arr已经被处理成后缀和的形式,所以arr[i] - arr[i + 1]得到原来的arr[i]。又因为数组从大到小排序了,所以一定i一定不会尝试到arr.size() - 1的位置,所以不用担心i + 1会越界
			for (; left > 0; left--)
			{
				if (arr[left - 1] > benefits)
				{
					break;
				}
			}
			cost = min(cost, (i + 1) * y + (left - i - 1) * x);  //统计最小的代价
		}
		cout << cost << endl;
	}
        return 0;
}

题目4(lc296)

给定一个矩阵,里面只有0和1,1代表有一个人在矩阵的这个位置,人只能上下左右移动,每次移动算一步,要求找到所有人花费的总步数最少的汇合点(或总步数)。

思路

由于人只能上下左右走,那么只看行的话,人只上下移动,如果所有人汇合到某一行花费的总步数最少,那么最佳汇合点一定在这一行,列同理,所以找到最佳汇合行和列后就找到了最佳汇合点。
那么如何找到最佳汇合行/列呢?流程是这样的:我们以行举例,首先统计每一行有多少个1,准备两个指针,分别指向最上面的行和最下面的行,准备两个变量sum1和sum2,分别统计上指针划过的行一共有多少个1、下指针划过的行一共有多少个1,初始值分别是第一行1的数量和最后一行1的数量。sum1和sum2哪个小哪个指针往中间移动,同时更新sum的值,每次移动都更新sum的值,然后下一次还是谁小谁移动,最后两个指针汇合的行就是最佳行。列同理。原理见https://www.bilibili.com/video/BV1g3411i7of?p=194&vd_source=77d06bb648c4cce91c6939baa0595bcd P 194

代码

int main()
{
	int n, m;
	cin >> n >> m;
	vector<vector<int>> matrix(n, vector<int>(m));
	vector<int> rowOnes(n);
	vector<int> colOnes(m);
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			cin >> matrix[i][j];
			if (matrix[i][j] == 1)
			{
				rowOnes[i]++;
				colOnes[j]++;
			}
		}
	}
	int i = 0;
	int j = n - 1;
	int iRest = 0;
	int jRest = 0;
	int total = 0;
	while (i < j)
	{
		if (rowOnes[i] + iRest <= rowOnes[j] + jRest)
		{
			total += rowOnes[i] + iRest;
			iRest += rowOnes[i];
			i++;
		}
		else
		{
			total += rowOnes[j] + jRest;
			jRest += rowOnes[j];
			j--;
		}
	}
	int row = i;
	i = 0;
	j = m - 1;
	iRest = 0;
	jRest = 0;
	while (i < j)
	{
		if (colOnes[i] + iRest <= colOnes[j] + jRest)
		{
			total += colOnes[i] + iRest;
			iRest += colOnes[i];
			i++;
		}
		else
		{
			total += colOnes[j] + jRest;
			jRest += colOnes[j];
			j--;
		}
	}
	int col = i;
	cout << row << " " << col << endl;
	cout << total << endl;
	return 0;
}

题目5(lc31)

整数数组的一个 排列  就是将其所有成员以序列或线性顺序排列。

例如,arr = [1, 2, 3] ,以下这些都可以视作 arr 的排列:[1, 2, 3]、[1, 3, 2]、[3, 1, 2]、[2, 3, 1] 。
整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

例如,arr = [1, 2, 3] 的下一个排列是 [1, 3, 2] 。
类似地,arr = [2, 3, 1] 的下一个排列是 [3, 1, 2] 。
而 arr = [3, 2, 1] 的下一个排列是 [1, 2, 3] ,因为 [3, 2, 1] 不存在一个字典序更大的排列。
给你一个整数数组 nums ,找出 nums 的下一个排列。

必须 原地 修改,只允许使用额外常数空间。

思路

从后往前遍历数组,找到i位置,i位置是使得arr[i] < arr[i + 1]的第一个数,这时从i+1开始往后是呈现“下坡”趋势,即i位置往后是字节序最大的排列方式,i位置之前不用变,i位置后面已经最大了,所以需要“下坡”趋势的数字里从后往前大于arr[i]的第一个数,与arr[i]交换,因为它是当前序列下一个序列中应该在i位置的数,此时后面还是“下坡”,把“下坡”反转以下变成“上坡”即可。需要注意的是如果遍历到数组最前面越界了还是没找到arr[i] < arr[i + 1],此时的数组序列是字典序最大的,反转整个数组即可。

代码

void reverse(vector<int>& arr, int i, int j)
{
	while (i < j)
	{
		int tmp = arr[i];
		arr[i++] = arr[j];
		arr[j--] = tmp;
	}
}

int find(vector<int>& arr, int i, int j, int num)
{
	int mid;
	int ans = 0;
	while (i <= j)
	{
		mid = i + (j - i) / 2;
		if (arr[mid] <= num)
		{
			j = mid - 1;
		}
		else
		{
			ans = mid;
			i = mid + 1;
		}
	}
	return ans;
}

int main()
{
	int n;
	cin >> n;
	vector<int> arr(n);
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	if (n < 2)
	{
		cout << arr[0] << endl;
		return 0;
	}
	int i;
	for (i = arr.size() - 1; i > 0; i--)
	{
		if (arr[i - 1] < arr[i])
		{
			i--;
			break;
		}
	}
	if (i == 0 && arr[i] > arr[i + 1])
	{
		reverse(arr, 0, arr.size() - 1);
	}
	else
	{
		int pos = find(arr, i + 1, arr.size() - 1, arr[i]);
		int tmp = arr[pos];
		arr[pos] = arr[i];
		arr[i] = tmp;
		reverse(arr, i + 1, arr.size() - 1);
	}
	for (int i = 0; i < n; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}

题目6

来自小红书
一个无序数组长度为n,所有数字都不一样,并且值都在[0...n-1]范围上
返回让这个无序数组变成有序数组的最小交换次数

思路

首先将整个数组离散化,即把所有数字按大小映射为[0...n-1]中的数字,如[30, 100, 50]变为[0, 2, 1],[5, 6, 7, 8, 9]变为[0, 1, 2, 3, 4]。这样做的目的是把数字与它排序后应该在的数组下标绑定,比如排序后,0就应该在0下标,1就应该在1下标......。此时从左到右i从0开始遍历,看看当前数字是否是在它排序后应该在的位置,如果不是,就把它放到它应该在的位置上,同时拿到此时占据它位置的数,对这个占据别人位置的数进行同样操作,每次操作总交换次数++,直到本轮的数都在它们应该在的位置上了,然后i++,继续前面的操作,直到i越界。

代码

int main()
{
	int n;
	cin >> n;
	vector<int> arr;
	for (int i = 0; i < n; i++)
	{
		cin >> arr[i];
	}
	map<int, int> indexMap;
	for (int i = 0; i < n; i++)
	{
		indexMap[arr[i]] = i;
	}
	int count = 0;
	for (auto it : indexMap)
	{
		arr[indexMap[it.second]] = count++;
	}
	int ans = 0;
	for (int i = 0; i < n; i++)
	{
		int index = i;
		while (index != arr[index])
		{
			int tmp = arr[index];
			arr[index] = index;
			index = tmp;
			ans++;
		}
	}
	cout << ans << endl;
}
posted @ 2022-10-04 10:49  小肉包i  阅读(50)  评论(0)    收藏  举报