算法笔记(4)
题目1
小明手中有n块积木,并且小明知道每块积木的重量。现在小明希望将这些积木堆起来
要求是任意一块积木如果想堆在另一块积木上面,那么要求:
- 上面的积木重量不能小于下面的积木重量
- 上面积木的重量减去下面积木的重量不能超过x
- 每堆中最下面的积木没有重量要求
现在小明有一个机会,除了这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执行以下两种操作中的一种,且每个数最多能执行一次操作:
- 可以选择让num变成 0,承担x的代价
- 可以选择让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;
}
浙公网安备 33010602011771号