第二节课:


题目一

在这里插入图片描述
算法原型:给定一个环形的单链表,从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;

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值