目录
数的范围(经典模板)
二分
数的范围
模板:判断需要的是二段性的左右
是第一段的末尾还是第二段的首段
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
int n,q;
scanf("%d%d",&n,&q);
int nums[n];
for(int i=0;i<n;i++)
scanf("%d",&nums[i]);
while(q--)
{
int target=0;
scanf("%d",&target);
int left=0;
int right=n-1;
while(left<right)
{
int mid=left+(right-left)/2;
if(nums[mid]>=target) right=mid;
else left=mid+1;
}
if(nums[left]!=target) cout<<"-1 -1"<<endl;
int j=left;
left=0;
right=n-1;
while(left<right)
{
int mid=left+1+(right-left)/2;
if(nums[mid]<=target) left=mid;
else right=mid-1;
}
//if(nums[right]!=target) cout<<"-1 -1"<<endl;
int k=right;
if(nums[right]==target)
cout<<j<<" "<<k<<endl;
}
return 0;
}
优化:第一次先求出第一次出现的位置,那么就已这个位置作为寻找这个数出现的末尾位置的left,从而缩短时间
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int q[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
for (int i = 0; i < m; i ++ )
{
int x;
scanf("%d", &x);
// 二分x的左端点
int l = 0, r = n - 1; // 确定区间范围
while (l < r)
{
int mid = l + r >> 1;
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
if (q[r] == x)
{
cout << r << ' ';
// 二分x的右端点
r = n - 1; // 右端点一定在[左端点, n - 1] 之间
while (l < r)
{
int mid = l + r + 1 >> 1; // 因为写的是l = mid,所以需要补上1
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << r << endl;
}
else cout << "-1 -1" << endl;
}
return 0;
}
数的三次方根
核心:由于是保留到底6位小数,所以我们要精确到第6位之后,浮点数判0的方式:小于一个非常小的数即可
#include<iostream>
#include<stdio.h>
using namespace std;
int main()
{
double x;
cin >> x;
double left = -10000;
double right = 10000;
while (right - left > 1e-8)
{
double mid = (right + left) / 2;
if (mid * mid * mid > x) right = mid;
else left = mid;
}
printf("%.6lf", left);
return 0;
}
机器人的跳跃问题
核心:可推知不管是上升还是下降:对能量的消耗都是E=2*E-h[i]
也有,当能量大于这个h数组的最大值时,那么一定可以使条件符合,那么要寻找的就是满足题意的第一个数,也就是二段性中的第二段的首段
check满足条件,那么就继续压缩right,因为要找到的是二段性中的第二段的首段
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
int h[N];
int n;
bool check(int mid)
{
for (int i = 1; i <= n; i++)
{
mid = mid * 2 - h[i];
if (mid > 1e5) return true;
if (mid < 0) return false;
}
return true;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &h[i]);
int left = 0;
int right = 1e5;
while (left < right)
{
int mid = left+(right-left)/2;
if (check(mid)) right = mid;
else left = mid + 1;
}
cout << left << endl;
return 0;
}
四平方和
核心:字典序最小,巧妙代换
暴力方法:
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 2500010;
int n;
int main()
{
cin >> n;
for (int a = 0; a * a <= n; a ++ )
for (int b = a; a * a + b * b <= n; b ++ )
for (int c = b; a * a + b * b + c * c <= n; c ++ )
{
int t = n - a * a - b * b - c * c;
int d = sqrt(t);
if (d * d == t)
{
printf("%d %d %d %d\n", a, b, c, d);
return 0;
}
}
}
二分:
核心:对pow(c,2)+pow(d,2)进行排序以及c和d,从而使这两个确定了最小字典序,
接着再遍历a,b确定这两个的最小字典序
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 2500010;
struct Sum
{
int s, c, d;
bool operator< (const Sum &t)const
{
if (s != t.s) return s < t.s;
if (c != t.c) return c < t.c;
return d < t.d;
}
}sum[N];
int n, m;
int main()
{
cin >> n;
for (int c = 0; c * c <= n; c ++ )
for (int d = c; c * c + d * d <= n; d ++ )
sum[m ++ ] = {c * c + d * d, c, d};
sort(sum, sum + m);
for (int a = 0; a * a <= n; a ++ )
for (int b = 0; a * a + b * b <= n; b ++ )
{
int t = n - a * a - b * b;
int l = 0, r = m - 1;
while (l < r)
{
int mid = l + r >> 1;
if (sum[mid].s >= t) r = mid;//也就是sum这个pow(c,2)+pow(d,2)太大了,需要right左移换个小的
else l = mid + 1;
}
if (sum[l].s == t)
{
printf("%d %d %d %d\n", a, b, sum[l].c, sum[l].d);
return 0;
}
}
return 0;
}
哈希:
核心:key存pow(c,2)+pow(d,2),同样地遍历a,b,确定这两个的最小字典序,接着利用map的快速查询
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_map>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 2500010;
int n, m;
unordered_map<int, PII> S;
int main()
{
cin >> n;
for (int c = 0; c * c <= n; c ++ )
for (int d = c; c * c + d * d <= n; d ++ )
{
int t = c * c + d * d;
if (S.count(t) == 0) S[t] = {c, d};
}
for (int a = 0; a * a <= n; a ++ )
for (int b = 0; a * a + b * b <= n; b ++ )
{
int t = n - a * a - b * b;
if (S.count(t))
{
printf("%d %d %d %d\n", a, b, S[t].x, S[t].y);
return 0;
}
}
return 0;
}
分巧克力
核心:理解 res+=(h[i]/mid)*(w[i]/mid);
mid此时是作为正方形的边长,也就说整个巧克力的面积除以枚举的正方形面积,同样的二段性:因为边长越长,分到的巧克力的个数越少,所以选取的是第一段的末尾:即刚好满足
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100010;
int h[N],w[N];
int n,k;
bool check(int mid)
{
int res=0;
for(int i=0;i<n;i++)
{
res+=(h[i]/mid)*(w[i]/mid);
if(res>=k) return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) scanf("%d%d",&h[i],&w[i]);
int left=1;
int right=1e5;
while(left<right)
{
int mid=left+right+1>>1;
if(check(mid)) left=mid;
else right=mid-1;
}
cout<<right<<endl;
}
前缀和
前缀和(经典模板)
因为开的数组足够大,为了方便从1开始对应
要求[left,right]区间的和的公式为:sum[right]-sum[left-1] (left>=1)
#include<iostream>
#include<cstdio>
using namespace std;
const int N=100010;
int nums[N];
int sum[N];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%d",&nums[i]);
sum[i]=sum[i-1]+nums[i];
}
while(m--)
{
int left,right;
cin>>left>>right;
printf("%d\n",sum[right]-sum[left-1]);
}
return 0;
}
子矩阵的和(经典模板)
核心:注意画图理解构造和求区间和公式的不同
#include<iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N];
int s[N][N];
int main()
{
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
scanf("%d", &a[i][j]);
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
}
}
while (q--)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
return 0;
}
激光炸弹
核心:因为这题数据比较大,所以为了节省空间,只开一个前缀和s数组,而不多开一个存权重w的数组,因此前缀和的构造公式需要发生小小的改变
此外:本题,没有限制地图的空间,只是给出了数据的范围,所以需要从给出的R的范围,和输入的x,y坐标中寻找最大的作为边界
思想:依次枚举每个R*R的矩阵,用前缀和数组快速地得出其区间和,记录最大值即可
#include<iostream>
#include<algorithm>
using namespace std;
const int N=5010;
int n,m;
int s[N][N];
int main()
{
int cnt,R;
cin>>cnt>>R;
R=min(5001,R);
n=m=R;
while(cnt--)
{
int x,y,w;
cin>>x>>y>>w;
x++,y++;//为了对应
n=max(n,x),m=max(m,y);
s[x][y]+=w;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
s[i][j]+=s[i-1][j]+s[i][j-1]-s[i-1][j-1];
int res=0;
for(int i=R;i<=n;i++)
for(int j=R;j<=m;j++)
res=max(res,s[i][j]-s[i-R][j]-s[i][j-R]+s[i-R][j-R]);
cout<<res<<endl;
return 0;
}
K倍区间
核心:从暴力->前缀和优化求和步骤->前缀和同余定理
思想:依次枚举右端点,并求出左端点和右端点之间的前缀和的余,如果有多少个余数相等,那么就将该个数加到res中即可 关于对cnt[0]=1的解释在注释中
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 100010;
/*没讲清楚,为什么前缀和模为1的时候会影响模为0的值,我认为是这样:总共出现了3次模为1的情况,
而每两次模为1组合起来可以模0,比如1,2加起来模为1,1,2,3,4,5加起来模为1,那么这两种情况组合起
来(区间做减)是3,4,5就是模为0的情况。所以一共出现了3次模为1的情况,那么两两组合的情况一共有三种,再
加上本来模为0的情况有3次,一共就6次模为0的情况。“k倍区间就加上cnt[sum[i]]”只是实现了计算模不为0的时候
的情况两两组合的组合数量*/
//答案等于前缀中出现过的和s[i]余k相等的计数之和,完成答案的计算之后需要将计数数组更新,加上自己
int n, k;
ll s[N], cnt[N];
int main()
{
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; i++)
{
scanf("%lld", &s[i]);
s[i] += s[i - 1];
}
ll res = 0;
cnt[0] = 1;
for (int i = 1; i <= n; i++)
{
res += cnt[s[i] % k];//已经构造好了:
cnt[s[i] % k]++;
}
cout << res << endl;
return 0;
}