题目一
算法原型:给定一个环形的单链表,从1开始计数,到m则杀死当前节点,在新的环中,后续节点继续从1开始计数,返回最终剩下的一个节点。约瑟夫环问题
如果用暴力方法,一共要杀死N-1个节点,每次需要数m,时间复杂度为:O(m*N)。下面将时间复杂度O(N)的解法:
最后存活的环的长度为1,假设节点的编号为1,是否存在一个函数f,传入的变量是:当前环的长度,和最终存货节点在当前环的编号,输出是上一次杀人之前该节点在这个环中的编号。y=f(x,i,m),其中,x是最终存货节点在当前环的编号,i是上一次杀人之前环的长度,m是固定变量计数周期,y是最终存活的节点在上一次杀人之前环的编号。如果存在这样的一个函数,那么就可以由最终存货的节点的编号1推出没杀人的时候环中的编号
y=x%i函数的图像:当发现类似这种图像时,就往该函数上靠。
先建立在没有杀人的情况下,节点的编号和报的号之间的关系:
下面看新旧节点之间的关系:
s是什么呢?s是老节点编号,因为每次数到m就杀人,使用s=(m-1)%i+1(第一个公式),i是老环的长度。所以老编号和新编号的关系是:
老=(新+(m-1)%i)%i+1
化简:
老=(新+m-1)%i+1
代码实现:
class Node {
public:
int val;
Node* next;
Node(int val) {
this->val = val;
this->next = nullptr;
}
};
//现在一共有i个节点,从编号为1的节点开始数,数到m就杀死节点,然后从杀死节点的下一个节点开始
//从1编号,然后还是从编号为1的节点开始数,数到m就杀死节点。
//最后只剩一个节点时存活下来,该节点的编号为1
//问这个存活下来的节点,在当前长度为i的环中的编号时多少
//调用getLive(N,m)就可以求出最终存活节点在没杀人的时候的编号
int getLive(int i, int m) {
if (i == 1) {
return 1;
}
return (getLive(i - 1, m) + m - 1) % i + 1;
}
Node* getLiveNode(Node* head, int m) {
if (head == nullptr || head->next == head) {
return head;
}
Node* p = head->next;
int len = 1;
while (p != head) {
len++;
p = p->next;
}
int i = getLive(len, m);
p = head;
for (; i > 1; i--) {
p = p->next;
}
return p;
}
原问题:还是这个公式,之前是数到m杀人,现在是数到给定数字杀人,只是每次m换个数。
老=(新+m-1)%i+1
代码实现:
int nextIndex(int size, int index) {
return index == size - 1 ? 0 : index + 1;
}
//还剩i个人,取用arr[index]数字
//返回那个人会活
int num(int i, vector<int>& arr, int index) {
if (i == 1) {
return 1;
}
return (num(i - 1, arr, nextIndex(arr.size(), index)) + arr[index] - 1) % i + 1;
}
//1..n个人围成一个圈,依次取用arr中的数字,每数到arr中的数字就杀死当前的人
//共杀n-1论
//返回最后或者的人在最开始的编号
int live(vector<int>& arr, int n) {
return num(n, arr, 0);
}
题目二
题目理解:
遍历每个点的最大高度,最大高度改变的点就是新轮廓线生成的点,高度就是当前点的最大高度,持续到下一次最大高度改变的点。这么感知最大高度变化了呢?
将原始数据进行处理,变为对每个点的描述:增加高度/减少高度,将处理后的数据根据第一维数据进行排序,如果第一维数据一样,加排在减的前面(防止存在单个点只存在高度的大楼)
根据处理后的数据生成轮廓线数组:用两个map,map1,key:高度,value:出现的次数;map2,key:坐标,value:最大高度
代码实现:
class Node {
public:
int index;
bool isAdd;
int height;
Node(int index, bool isAdd, int height) {
this->index = index;//坐标
this->isAdd = isAdd;//增加高度/降低高度
this->height = height;//增加/降低多少高度
}
};
bool cmp(Node* x, Node* y) {
if (x->index != y->index) {
return x->index < y->index;
}
return x->isAdd > y->isAdd;
}
vector<vector<int>> outline(vector<vector<int>>& arr) {
//数据预处理:按照坐标排序
vector<Node*>nodes(2*arr.size());
for (int i = 0; i < arr.size(); i++) {
nodes[i] = new Node(arr[i][0], true, arr[i][2]);
nodes[i+arr.size()] = new Node(arr[i][1], false, arr[i][2]);
}
sort(nodes.begin(), nodes.end(), cmp);
//生成两个map
map<int, int, greater<int>>mp1;//高度:次数
map<int, int>mp2;//位置:最大高度
for (int i = 0; i < nodes.size(); i++) {
if (nodes[i]->isAdd) {
mp1[nodes[i]->height]++;
}
else {
if (mp1[nodes[i]->height] == 1) {
mp1.erase(nodes[i]->height);
}
else {
mp1[nodes[i]->height]--;
}
}
mp2[nodes[i]->index] = mp1.empty() ? 0 : mp1.begin()->first;
}
//根据map2生成轮廓线
vector<vector<int>>res;
int pre = mp2.begin()->first;//上一次高度变化的位置
for (map<int, int>::iterator it = mp2.begin(); it != mp2.end(); it++) {
if (it->second != mp2[pre]) {
if (mp2[pre] != 0) {//高度为0,说明没有楼
res.push_back({ pre,it->first,mp2[pre] });
}
pre = it->first;
}
}
return res;
}
题目三
看到正数数组,构建单调性:双指针l和r,开始时位于数组的左侧,双指针之间的窗口累加和小于k,r移动;大于k,l移动;等于k,获得一个答案,r移动;直到r越界为止。
代码实现:
int maxLength(vector<int>& arr, int k) {
if (arr.size() == 0) {
return 0;
}
int res = 0;
int sum = arr[0];
int r = 0;
int l = 0;
while (r < arr.size()) {
if (sum == k) {
res = max(res, r - l + 1);
sum -= arr[l];
l++;
}
else if (sum > k) {
sum -= arr[l];
l++;
}
else {
r++;
if (r == arr.size()) {
break;
}
sum += arr[r];
}
}
return res;
}
题目四
给定一个无序数组,数组的值可正、可负、可为0,在给定一个正数k。求arr的所有子数组中元素相加和为k的最长子数组长度。
看到子数组问题,就想以某一位置结尾的情况:求数组的前缀和数组;用map记录某一前缀和最找出现的位置;遍历原数组,在map中查找前缀和为presum[i]-k的最找位置,此时求得长度就是以i位置结尾的子数组的累加和为k的最长子数组长度。
代码实现:
int maxLength(vector<int>& arr, int k) {
if (arr.size() == 0) {
return 0;
}
vector<int>presum(arr.size());//可以优化掉
presum[0] = arr[0];
unordered_map<int, int>mp;//前缀和:最找出现的位置
mp.insert({ presum[0],0 });
mp.insert({ 0,-1 });//累计和为0的情况
int res = 0;
for (int i = 1; i < arr.size(); i++) {
presum[i] = presum[i - 1] + arr[i];
if (mp.find(presum[i]) == mp.end()) {
mp.insert({ presum[i],i });
}
if (mp.find(presum[i] - k) != mp.end()) {
res = max(res, i - mp[presum[i] - k]);
}
}
return res;
}
题目五
由原数组构建两个数组,minSum:从i位置出发的子数组,最小sum;minSumEnd,从i位置出发的子数组获得最小sum时的右边界
初始时,从0位置开始,看0位置开始的最小累加和是否超过了k,如果没超过,则来到0位置最小累加和的右边界的下一个位置,看这一位置开始的最小累加和是否超过了k-0位置开始的最小累加和,如果没有,…;如果超过了,则从0位置开始到最后一块没超过的长度就是以0位置开始能得到的最长符合要求的子数组长度
得到的窗口不需要回退,将0位置移出窗口,来到1位置,看有没有可能将之前窗口的下一位置放到窗口中。1位置开始符合条件的右边界可能在之前窗口的左边,但是不关心小于0位置得出的答案(舍弃可能性);如果窗口变为0,则重新从下一位置开始一个窗口
代码实现:
int maxLength(vector<int>& arr, int k) {
if (arr.size() == 0) {
return 0;
}
vector<int>minSums(arr.size());
vector<int>minSumEnds(arr.size());
minSums[arr.size() - 1] = arr[arr.size() - 1];
minSumEnds[arr.size() - 1] = arr.size();
for (int i = arr.size() - 2; i >= 0; i--) {
if (minSums[i + 1] <= 0) {
minSums[i] = minSums[i + 1] + arr[i];
minSumEnds[i] = minSumEnds[i + 1];
}
else {
minSums[i] = arr[i];
minSumEnds[i] = i;
}
}
int res = 0;
int sum = 0;//窗口累加和
int end = 0;
//end:窗口右边界的下一个位置
//i窗口左边界
for (int i = 0; i < arr.size(); i++) {//不回退
while (end < arr.size() && sum + minSums[end] <= k) {//向右扩的条件
sum = sum + minSums[end];
end = minSumEnds[end] + 1;
}
res = max(res, end - i);
if (end > i) {//尝试将end位置的数纳入进来
sum = sum - arr[i];
}
else {//窗口内没数了,说明从i开头的所有子数组累加和都不可能<=k,右边界移动
end = i + 1;
}
}
return res;
}
题目六
尼姆博弈问题:将所有数字异或起来,如果结果是非零,则先手赢;否则后手赢。Why?
先手的目标:自己拿完最后的硬币,或者最先让对手面对无硬币的局面–>先手拿完后,变成异或为0的情况,最后的状态也是异或为0,如果每一步都能做到,那么先手必赢。可以做到,但前提是,一开始时的异或不能为0,这样后手这么拿,异或均不可能变成0,然后先手再使异或为0
代码实现:
//返回true,先手赢
//返回false,后手赢
bool whoWin(vector<int>& arr) {
if (arr.size() == 0) {
return false;
}
int eor = arr[0];
for (int i = 1; i < arr.size(); i++) {
eor = (eor ^ arr[i]);
}
return eor == 0 ? false : true;
}
题目七
伪进制:正常的k进制是,每个位的状态是0…k-1,而伪k进制每个位的状态是1…k。伪k进制每个位必须有数(至少为1),首先给每一位分一个1,将总数减去各个位分去的数,再将剩余的数,从高位向低位分。伪k进制可以表示任何一个正数。
代码实现:
//伪k进制
int str2Num(vector<char>& arr, string& s) {
int k = arr.size();//伪k进制
if (k < 1 || k>26) {
return -1;//无效值
}
if (s.length() == 0) {
return 0;
}
int res = s[0] - 'A' + 1;
for (int i = 1; i < s.length(); i++) {
res = res * k + s[i] - 'A' + 1;
}
return res;
}
string num2Str(vector<char>& arr, int num) {
int k = arr.size();
if (num < 1) {
return "";
}
string res = "";
int rest = num;
vector<int>v;//记录各个位上的数字,便于后续转成字符
int bit = 1;//k的0次 k的1次 k的2次...
while (rest>=bit) {
v.push_back(1);
rest -= bit;
bit *= k;
}
for (int i = v.size() - 1; i >= 0; i--) {
bit /= k;
int q = rest / bit;
v[i] += q;
res += arr[v[i] - 1];
rest -= q * bit;
}
return res;
}