题目一
给定一个有序数组arr,代表数轴上从左到右有n个点arr[0]、arr[1]...arr[n-1],给定一个正数L,代表一根长度为L的绳子,求绳子最多能覆盖其中的几个点。
考点: | 二分 |
难度: | 一面或者二面的第一题 |
思路:
从左向右依次利用二分查找算法找到数组中每个位置 end 作为终点时,绳子L所能覆盖的最大距离(即第一个值大于等于value的点index与end之间的差值)。取所有的距离中最大的作为最终结果。(做梦也没想到,再次掏出数位板,居然是为了写博客)
代码:
/*
给定一个有序数组arr,代表数轴上从左到右有n个点arr[0]、arr[1]...arr[n-1],
给定一个正数L,代表一根长度为L的绳子,求绳子最多能覆盖其中的几个点。
*/
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
//查找[0,end]中第一个值大于等于value的索引位置index
int nearestIndex(vector<int>&nums, int end, int value){
if(value<0) return 0;
//计算起点与数组终点的距离
int begin=0;
int mid=0, index=end;
while(begin<end){
//>>1 除以2,这种求中值的方法可以防止溢出
mid=begin+((end-begin)>>1);
if(nums[mid]>=value){
//记录当前值大于等于value的点的索引位置index
index=mid;
end=mid-1;
}else{
begin=mid+1;
}
}
return index;
}
int maxPoint(vector<int>&nums, int L){
if(L<=0 || nums.size()<1) return 0;
int res=1, nearest=0;
for(int i=0; i<nums.size(); i++){
nearest=nearestIndex(nums, i, nums[i]-L);
res=max(res, i-nearest+1);
}
return res;
}
int main(){
std::vector<int>line={1,2,2,2,2,3,3,3,3,3,3,3,5,5,5 };
int L = 2;
std::cout<< maxPoint(line, L) << std::endl;
return 0;
}
题目二
小虎去附近的商店买苹果,奸诈的商贩使用了捆绑交易,只提供6个每袋和8个每袋的包装包装不可拆分。可是小虎现在只想购买恰好n个苹果,小虎想购买尽量少的袋数方便携带。如果不能购买恰好n个苹果,小虎将不会购买。输入一个整数n,表示小虎想购买的个苹果,返回最小使用多少袋子。如果无论如一何都不能正好装下,返回-1。
思路
方法一对于苹果数 n,尽可能多的使用能装8个苹果的袋子,余下的用能装6个苹果的袋子去装。
假如 n=42,则先用5个能装8个苹果的袋子去尝试装苹果,此时用袋子数为 6=5+1(能装6个苹果的袋子数)。然后尝试用4个能装8个苹果的袋子去装苹果,此时余下苹果数为10,袋子数为6=4+2...
若当前刨除用8个苹果的袋子所装苹果之后余下的苹果数大于24,就不用继续往下试了,因为24=3*8=4*6,4>3
方法二:通过打表找到规律
0-17 | 没有规律 | 奇数苹果返回值为-1 | |
18-25 | 8的个数2 | 奇数苹果返回值为-1 | 偶数返回值为3 |
26-33 | 8的个数3 | 奇数苹果返回值为-1 | 偶数返回值为4 |
34-41 | 8的个数4 | 奇数苹果返回值为-1 | 偶数返回值为5 |
42-49 | 8的个数5 | 奇数苹果返回值为-1 | 偶数返回值为5 |
因此,遇到苹果时,如果小于17,则通过方法一计算,大于17,则看他落在那个区间就可以了,不用继续算
代码:
#include<iostream>
using namespace std;
//用于生成0-x的随机数
#define random(x) (rand()%x)
//方法一
int calculate(int n){
//奇数个或者少于6个肯定不会购买
if((n<=5)||(n%2!=0)) return -1;
int max=n/8;
//边界条件为24,是因为4*6=3*8
while((max>=0) && (n-max*8<24)){
if((n-max*8)%6==0)
return max+(n-max*8)/6;
max--;
}
return -1;
}
//第二种方法:通过打表发现规律
int minBag(int n){
//n & 1!=0 意思就是n的二进制位最后一位不为0,则n肯定是奇数(抖机灵)
if((n<=5)||(n & 1!=0)) return -1;
if(n<18){
return ((n==6)||(n==8)) ? 1: ((n==12)||(n==14)||(n==16)) ? 2 : -1;
}
return (n-18)/8+3;
}
//写一个对数器
int main(){
int n=0, testTime=1000000, test1=0, test2=0;
for(int i=0; i<testTime; i++){
n=random(1000);
test1=calculate(n);
test2=minBag(n);
if(test1!=test2){
cout<<n<<endl;
cout<<"fuck!!!"<<endl;
break;
}
}
return 0;
}
题目三
牛牛有一些排成一行的正方形。每个正方形已经被染成红色或者绿色。牛牛现在可以选择任意一个正方形然后用这两种颜色的任意一种进行染色,这个正方形的颜色将会被覆盖。牛牛的目标是在完成染色之后,每个红色R都比每个绿色G距离最左侧近。
牛牛想知道他最少需要涂染几个正方形。如样例所示: s = RGRGR我们涂染之后变成RRRGG满足要求了,涂染的个数为2,没有比这个更好的涂染方案。(就是左侧都变成R,右侧都是G,最少涂几个)
思路:
依次计算数组中每个位置左侧需要涂成R的个数和右侧涂成G的个数,然后找到最小值。此时时间复杂度是O(n*n),因为对于每一个位置,都要把整个数组的每个位置都考虑一遍。
优化:——预处理数组的技巧
准备两个数组,一个用于统计当前位置右侧有几个R,一个用于统计当前位置左侧有几个G。之后每次遍历的时候,直接从数组中取值,省去了遍历行为。
遍历数组,先从左到右的计算中每个位置中left_G数组所对应的值,再从右向左的,计算出right_R中的值。
还能再优化——left_G中的值是能够通过数组right_R和数组长度计算出来的,因此只用数组right_R就可以了。
代码
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
//用一个数组right_R从右往左记录当前位置以及其右侧有多少个R
//然后从左往右遍历。用Gnum记录当前位置以及其左侧共有多少个R
//取最小的和即为最小的涂改次数
//用空间换时间
int calculate(string input){
int len=input.length();
if(len<=0) return 0;
//Gnum用于记录,当前位置时,数组已经出现过的G的个数
int res=len, Gnum=0, Rnum=0;
vector<int>right_R(len);
for(int i=len-1; i>=0; i--){
if(input[i]=='R') Rnum++;
right_R[i]=Rnum;
}
for(int i=0; i<len; i++){
//考虑0位置前面,0-1位置,1-2位置,,,,最后一个位置后面的位置
res=min(res, Gnum+right_R[i]);
//不断积累已经出现过的G的个数
if(input[i]=='G') Gnum++;
}
//考虑把方形全都涂成R的情况
res=min(res,Gnum);
return res;
}
int main(){
string input;
cin>>input;
int result=calculate(input);
cout<<result<<endl;
return 0;
}
题目四
给定一个N*N的矩阵matrix,只有0和1两种值,返回边框全是1的最大正方形的边
长长度。
例如:
01111
01001
01001
01111
01011
其中边框全是1的最大正方形的大小为4*4,所以返回4。
思考
- 一个矩阵有对少个子矩阵?
- 一个矩阵中有多少正方形?
答案:
1、一个n*n的矩阵中有n的四次幂个子矩阵
在矩阵中随机选择一个点,这个点有n*n种的可能性。在这个矩阵中,再选择一个位置,可能性也是n*n种。用这两个点分别作为矩阵的左上角和右下角,就是一个子矩阵。对于每个子矩阵,发生重复的次数是1次,即
和发生重复
因此n*n的矩阵有n的四次幂种子矩阵。
2、一个n*n的矩阵中,有n的三次幂个正方形子矩阵
在矩阵中随机选择一个点,这个点有n*n种的可能性。在这个矩阵中,再选择一个位置,可能性也是n*n种。以这个点为正方向的左上角,他的边长有n中可能性。因此一个n*n的矩阵中有n的三次幂个正方形的子矩阵。
本题思路
本题的技巧是预处理数组——当你发现每次的查询都是通过遍历来搞定,此时需要思考能否使用预处理数组来进行搞定
对于矩阵有n的三次幂种正方形子矩阵,则对于没有个正方形,我们需要验证一下他的四个边是否都是1,则加上验证之后的时间复杂度变成O(n的四次幂)
制作两个预处理数组:
- right[i][j]——原始矩阵中[i][j]这个点的右侧有几个连续的1。
- down[i][j]——原始矩阵中[i][j]这个点的下侧有几个连续的1。
原始数组 | right[i][j] | down[i][j] |
有了预处理数组,对于矩阵中的每个点 [x][y] 作为正方形子矩阵的左上角,假设正方形边长为L。则只需要验证
- right[x][y]是否大于L
- down [x][y]是否大于L
- down[x+L][y]是否大于L
- right[x][y+L]是否大于L
此时的时间复杂度为:
O(n*n*n)=(构架预处理数组的时间复杂度O(n*n))+(遍历矩阵中每个点,考虑每个点作为正方形左上角的每种边长发生的情况O(n*n*n))
代码:
#include<iostream>
#include<vector>
using namespace std;
int calculate(vector<vector<int> >num,
vector<vector<int> >right, vector<vector<int> >down) {
//正方形的边长最长是最短的矩阵边的值
int len = num[0].size() < num.size() ? num[0].size() : num.size();
int max = 0;
//考虑矩阵中每个点作为正方形的左上角点的每种边长的情况
//max记录所有正方形子矩阵中最长的边长
for (int i = 0; i < num[0].size(); i++) {
for (int j = 0; j < num.size(); j++) {
for (int k = len; k > 0; k--) {
if ((num[j][i]==1)&&
(right[j][i] >= k) &&
(down[j][i] >= k)&&
(k>max)) {
max = k;
break;
}
}
}
}
return max;
}
//构建预处理数组——right和down
void construct(vector<vector<int> >num,
vector<vector<int> >&right, vector<vector<int> >&down) {
//初始化right
for (int i = num[0].size() - 1; i >= 0; i--) {
for (int j = 0; j < num.size(); j++) {
if (num[j][i] == 1) {
if (i == num[0].size() - 1) {
right[j][i] = 1;
}
else if (num[j ][i+1] == 1) {
right[j][i] = right[j ][i+1] + 1;
}
else if (num[j ][i+1] == 0) {
//不可以和第二个if合并
//如果i==num[0].size()-1,再去查num[i+1][j]会发生指针越界
right[j][i] = 1;
}
}
}
}
//初始化down
for (int i = num.size() - 1; i >= 0; i--) {
for (int j = 0; j < num[0].size(); j++) {
if (num[i][j] == 1) {
if (i == num.size() - 1) {
down[i][j] = 1;
}
else if (num[i+1][j] == 1) {
down[i][j] = down[i + 1][j] + 1;
}
else if (num[i + 1][j] == 0) {
down[i][j] = 1;
}
}
}
}
}
int main() {
vector<vector<int> >num = { {1,1,1,0,1,1},{1,0,1,1,1,1},{1,1,1,0,0,1},{1,0,1,0,0,1},{1,1,1,1,1,1} };
//二维数组初始化
//vector<vector<int>> vec(10, vector<int>(10));
vector<vector<int> >right(num.size(), vector<int>(num[0].size()));
vector<vector<int> >down(num.size(), vector<int>(num[0].size()));
construct(num, right, down);
cout << calculate(num, right, down) << endl;
return 0;
}
题目五
给定一个函数f,可以1~5的数字等概率返回一个。请加工出1~7的数字等概率返回一个的函数g。
给定一个函数f,可以a~b的数字等概率返回一个。请加工出c~d的数字等概率返回一个的函数g。
给定一个函数f,以p概率返回0,以1-p概率返回1。请加工出等概率返回0和1的函数g
思路:
这种类型的题,需要将题目中给予的条件转换成只能等概率返回0和1的函数,然后将其改装成等概率返回0-k的函数即可(改装方式利用二进制位)
比如第一个,可以返回1-5数字的等概率函数,可以设置该函数
- 在返回1和2时,返回值是0,
- 在返回3和4时,返回值是1;
- 在返回5时,重新调用一遍该函数。
如此,可以保证0和1的等概率返回。
目标是1-7,如果我们能把上面等概率返回0和1的函数改装成返回0-6的函数,在对应的返回值进行加1操作,便可得到返回1-7的目标函数。
因此,该题目变成,将等概率返回0和1的函数改装成等概率返回0-k的函数。
比如我拥有一个可以等概率返回0和1的函数,我们要将其改装成等概率返回0-17的函数的步骤
- 计算出17有几个二进制位,得出需要五个二进制位
- 对于每个二进制位的值,调用等概率返回0和1的函数来决定
- 由于5位二进制可表示的范围是0-31,。对于大于17的值,重新调用函数去计算五个二进制位的值即可
将返回值为1-5的函数加工成返回值是0-x的函数的实现代码:
#include<iostream>
using namespace std;
//已知条件,一个等概率返回0-5的函数。
//我们要将其制作成等概率返回0和1的函数
int one_t_five(){
int low=1;
int high=5;
return rand()%(high-low+1)+low;
}
//制作等概率返回0和1的函数发生器
int o_and_one(){
int res=0;
do{
res=one_t_five();
}while(res==5);
return (res==0 || res==1)?0:1;
}
//计算需要几位二进制比特位
int calculate(int n){
int needBit=1;
while( (1<<needBit)-1 <n ){
needBit++;
}
return needBit;
}
int make_func(int n){
int res=0;
//获取n有几位二进制
int needBit=calculate(n);
//计算每一位的二进制
for(int i=0; i<needBit; i++){
res+=(o_and_one()<<i);
}
if(res<=n){
return res;
}else{
make_func(n);
}
}
int main(){
int n;
cout<<"请输入一个正整数"<<endl;
while(cin>>n){
cout<<"制作等概率返回0-"<<n<<"的函数发生器"<<endl;
cout<<make_func(n)<<endl;
}
return 0;
}
对于题目中的最后一问:
0的概率是p,得到1的概率是1-p。则将函数每次都运行两次,这两次分别代表一个两位二进制数中两个位置的值。
如果得到的结果为
- 00或者11,则重新调用函数。因为00的概率是p*p,11的概率是(1-p)*(1-p),因此我们用不了。
- 01 则返回0
- 10 则返回1
如此,该函数被加工成等概率返回0和1的函数。实现代码就不写了。
题目六
给定一个非负整数n,代表二叉树的节点个数。返回能形成多少种不同的二叉树结构
思考
0个节点的结果是1——空树
1个节点的结果是1
2个节点的结果是2
20个节点怎么求? 可以有以下划分
- 左子树没有节点,右子树有19个节点
- 左子树有1个节点,右子树有18个节点
- .....
- 左子树19个节点,右子树0个节点
对于一棵左子树有7个节点,右子树有12个节点的二叉树。假设左子树有a种类划分方式,右子树有b种划分方式,则整棵树有a*b种划分方式。
实现代码(暴力递归。暴力递归转动态规划的方法会在高级班进行讲解,加油好好听课,快点学吧,呜呜呜):
#include<iostream>
using namespace std;
int calculate(int n){
if(n<0) return 0;
if(n==0) return 1;
if(n==1) return 1;
if(n==2) return 2;
int res=0;
//因为根节点占一个节点,因此划分的数量不会到达n,因此left<n-1
for(int left=0; left<n-1; left++){
int l=calculate(left);
int r=calculate(n-left-1);
res+=l*r;
}
return res;
}
int main(){
int n=0;
cout<<"请输入树的节点个数"<<endl;
cin>>n;
cout<<calculate(n)<<endl;
return 0;
}
题目七
一个完整的括号字符串定义规则如下:
- ①空字符串是完整的。
- ②如果s是完整的字符串,那么(s)也是完整的。
- ③如果s和t是完整的字符串,将它们连接起来形成的st也是完整的。
例如,"(()())", ""和"(())()"是完整的括号字符串,"())(", "()(" 和 ")"是不完整的括号字符串。
牛牛有一个括号字符串s,现在需要在其中任意位置尽量少地添加括号,将其转化为一个完整的括号字符串。请问牛牛至少需要添加多少个括号。
思考
如何判断一个字符串是否是合法的括号字符串?
对于一个符号字符串“()(())()”,使用count变量。遇到 “(”,则执行count++;遇到“)”,则执行count--。
- 当count出现count<0的情况,说明字符串不合法。
- 当遍历结束后,count的值如果为0,则字符串是合法的符号字符串;否则是不合法的。
那么对于这个题。如果想凑成合法的符号字符串,我们需要满足两个标准
- 从左到右遍历时,不可以出现 “)”比“(”多的情况
- 遍历结束之后,需要多少“)”与字符串中剩余的“(”进行配对。
实现代码
#include<iostream>
using namespace std;
int calculate(string input){
if(input.length()<=0) return 0;
int count=0, needRight=0;
for(int i=0; i<input.length(); i++){
if(input[i]=='('){
count++;
}else if(input[i]==')'){
if(count<=0){
needRight++;
}else{
count--;
}
}else{
cout<<"input error"<<endl;
return 0;
}
}
return count+needRight;
}
int main(){
string input="(())))())";
cout<<calculate(input)<<endl;
return 0;
}
生成随机数组的模板,生成随机二叉树的模板