常用算法代码模板与技巧
欢迎大家订阅我的专栏:算法题解:C++与Python实现!
本专栏旨在帮助大家从基础到进阶 ,逐步提升编程能力,助力信息学竞赛备战!
专栏特色
1.经典算法练习:根据信息学竞赛大纲,精心挑选经典算法题目,提供清晰的代码实现与详细指导,帮助您夯实算法基础。
2.系统化学习路径:按照算法类别和难度分级,从基础到进阶,循序渐进,帮助您全面提升编程能力与算法思维。
适合人群:
- 准备参加蓝桥杯、GESP、CSP-J、CSP-S等信息学竞赛的学生
- 希望系统学习C++/Python编程的初学者
- 想要提升算法与编程能力的编程爱好者
这篇文档其实是我自己学习路上的一份“复习笔记”。之前看了不少教程,零零散散记了一堆代码片段和小技巧,时间久了就容易忘,翻起来也麻烦。所以干脆花时间整理了一下,把常用的模板、容易踩的坑、还有一些个人觉得挺实用的小技巧都归到了一起。
如果你也在学类似的东西,或许能帮你省下一些翻找的时间;如果有哪里写得不清楚或不对,也欢迎随时指正交流。
希望这份整理也能对你有一点点帮助~ ✨
代码技巧
11、8的倍数
// 11的倍数:奇数位之和 与 偶数位之和 的差是11的倍数
for (int i=0; i<a.size(); i++)
{
if (i%2==0) ji += a[i]-'0';
else ou += a[i]-'0';
}
if ((ji-ou)%11==0) cout << "Yes" <<endl;
else cout << "No" << endl;
// 8的倍数:后三位是8个倍数,i=5时,有i-1个数是8的倍数
for (int i=0; i<s.size(); i++)
{
if ((s[i]-'0')%8==0) cnt++;
if (i>=1 && ((s[i-1]-'0')*10 + (s[i]-'0')) % 8 == 0) cnt++;
if (i>=2 && ((s[i-2]-'0')*100 + (s[i-1]-'0')*10 + (s[i]-'0')) % 8 == 0) cnt += i-1;
}
金币发放
int ci=0, jin=1, cnt1=0;
for (int i=1; i<=m; i++)
{ // m为天数
cnt1 += jin; // cnt1为总金币数,jin为每天发放的金币数
ci++; // 持续天数
if (ci==jin)
{
jin++;
ci=0;
}
}
数位拆分
void chai(int n)
{
if (n==0) flag[0]=1;
while (n>0)
{
flag[n%10]=1; // 这里可以实现数码出现与否的统计,或数码出现次数的统计,或统计数位和
n/=10;
}
}
约瑟夫环
int n, m; // n个人,数到m出列
int id=-1; // id从-1开始,以便初始下标为0
for (int i=1; i<=n; i++)
{ // n个人
for (int j=1; j<=m; j++)
{ //数到m出列
id++; // id初始从-1开始
id%=n;
if (id<0) id += n; // 处理id递减的场景
while (flag[id]==0)
{
id++;
id%=n;
}
}
flag[id]=0; // 数到m的人出列
}
//(编号+n-1)%n+1
最长连续值
// 求字符串中最长连续xx字符
int cnt, cur=0;
for (int i=0; i<s.size(); i++)
{
if (s[i]=='b') cnt++;
else a[++cur] = cnt;
}
a[++cur]=cnt; // 一定要加上这句,将最后的结果也保存进a数组中
回文字符串判断
bool isPal(string s)
{
string s1="";
for (int i=s.size()-1; i>=0; i--)
s1 += s[i];
return s1==s;
}
// 回文判断加速代码(设定开始和结束指针,从两边向中间靠拢)
for (int i=0; i+k-1<n; i++)
{
int j = i+k-1;
for (int x=i, y=j; x<y; x++, y--)
if (path[x]!=path[y]) return false;
}
return true;
多重比较
// 依次看中总成绩、速写成绩、色彩成绩、常规素描成绩
bool cmp(Stu x, Sty y) {
if (x.zf!=y.zf) return x.zf>y.zf;
if (x.sx!=y.sx) return x.sx>y.sx;
if (x.sc!=y.sc) return x.sc>y.sc;
if (x.sm!=y.sm) return x.sm>y.sm;
}
矩阵对角线
// 左上至右下
a[i][i]
// 左下至右上
a[i][n-i+1]
// 与格子(x,y)同一左上至右下对角线
x-y == i-j
// 与格子(x,y)同一左下至右上对角线
x+y == i+j
走格子路线
// 左上(1,)至右下(n,)的路线数
b[i][j] = b[i-1][j] + b[i][j-1]
杨辉三角形
for (int i=0; i<=n; i++)
{
a[i][0] = 1;
for (int j=0; j<=i; j++)
a[i][j] = a[i-1][j-1] + a[i-1][j];
}
矩阵翻转与旋转
// a[]为原始矩阵,b[]为变换后的矩阵
// 上下翻转
for (int i=1; i<=n; i++)
{ // n为行
for (int j=1; j<=m; j++)
{ // m为列
b[i][j] = a[n-i+1][j];
}
}
// 左右翻转
for (int i=1; i<=n; i++)
{ // n为行
for (int j=1; j<=m; j++)
{ // m为列
b[i][j] = a[i][m-j+1];
}
}
// 向右旋转90度
for (int i=1; i<=m; i++)
{ // m为列
for (int j=1; j<=n; j++)
{ // n为行
b[i][j] = a[n-j+1][i];
}
}
// 输出时先遍历列,再遍历行,输出b数组
for (int i=1; i<=m; i++)
{
for (int j=1; j<=n; j++)
cout << b[i][j] << " ";
cout << endl;
}
// 向左旋转90度
for (int i=1; i<=m; i++)
{ // m为列
for (int j=1; j<=n; j++)
{ // n为行
b[i][j] = a[j][m+1-j];
}
}
// 输出时先遍历列,再遍历行,输出b数组
// 旋转180度
for (int i=1; i<=n; i++)
{ // n为行
for (int j=1; j<=n; j++)
{ // m为列
b[i][j]=a[n+1-i][m+1-j];
}
}
// 输出时先遍历行,再遍历列,输出b数组
// 矩阵A转置(A^T)
for (int i=1; i<=m; i++)
{
for (int j=1; j<=n; j++)
{
cout << a[j][i] << " ";
}
cout << endl;
}
矩阵查找
for (int i=1; i<=n-m+1; i++) // 枚举大矩阵的行列坐标
for (int j=1; j<=n-m+1; j++)
{
bool flag = true;
for (int a=1; a<=m; a++) // 枚举小矩阵的行列坐标
for (int b=1; b<=m; b++)
if (s[a+i-1][b+j-1]!=t[a][b]) flag = false; // 在大矩阵的行列坐标下扩展小矩阵
if (flag) cout << i << " " << j << endl; // flag==true说明大矩阵以(i,j)为起点的矩阵与小矩阵完全匹配
}
进制转换
进制间转换
// 十进制转十六进制(也适用于十进制转二进制)
string DtoH(int n)
{
string d="0123456789ABCDEF",ans="";
if (n==0) return "0"; //对于n为0的情况,需要这样特判
while (n>0)
{
ans=d[n%16]+ans;
n/=16;
}
return ans;
}
// 十进制转m进制
string Dtom(int n,int m)
{
string d="0123456789ABCDEF",ans="";
while (n>0)
{
ans=d[n%m]+ans;
n/=m;
}
return ans;
}
// 二进制转十进制
int BtoD(string s)
{
int ans=0;
for (int i=0;i<s.size();i++)
{
ans=ans*2+s[i]-'0';
}
return ans;
}
// 十六进制转十进制
int HtoD(string s)
{
int ans=0;
for (int i=0;i<s.size();i++)
{
if (s[i]<='9')
ans=ans*16+s[i]-'0';
else
ans=ans*16+s[i]-'A'+10;
}
return ans;
}
// m进制转十进制
int mtoD(string s,int m)
{
int ans=0;
for (int i=0;i<s.size();i++)
{
if (s[i]<='9')
ans=ans*m+s[i]-'0';
else
ans=ans*m+s[i]-'A'+10;
}
return ans;
}
保留进制转换后每个位数的数字
// 将数字转为k进制后,保存每位的数字,s[]保存每个x的转为k进制后,每个位数上的数字
void f(int x)
{
for (int j=0; x; j++, x/=k)
s[j] += x%k;
}
题解:洛谷 B4499 [GESP202603 三级] 二进制回文串
约数和
for (int i=1; i<=n; i++)
for (int j=2; j<=n/i; j++)
fsum[i*j] += i; // fsum[i*j] 数x=i*j 的所有约数和
4个方向和8个方向的定义
// 4个方向
int dx[4] = {-1,1,0,0} // 上下左右
int dy[4] = {0,0,-1,1};
// 8个方向
int dx[8] = {-1,-1,-1,0,1,1,1,0};
int dy[8] = {-1,0,1,1,1,0,-1,-1};
字符、数字与坐标转换
用字符串描述3*3二维矩阵中的字符并做上下左右更换
//将字符串中'x'的位置找出
string t = "123456x78"
int k = t.find('x');
int x = k/3, y = k%3;
//更换字符串中与'x'字符上下左右相邻的字符
for (int i=0; i<4; i++)
{
int a = x+dx[i], b = y +dy[i];
if (a>=0 && a<3 && b>=0 && b<3)
{
swap(t[k], t[a*3+b]]);
// BFS的判断及队列添加(略)
swap(t[k], t[a*3+b]]); //需要还原现场
}
}
数字转换为坐标
// a[i]为数字,转换为坐标
double y = ceil(a[i]*1.0/n); // 坐标转换
int x = a[i] - ((y-1)*n);
vis[x][int(y)] = 1; //标记
数值转二维平面坐标
// 输入数值为t,转为{x, y}坐标
PII getpos(int t)
{
int x = (t + n - 1) / n;
int y = t % n;
if (y==0) y = n;
return {x, y};
}
两个线段之间求交集
// 一个区间的左右端点为l,r,另一个区间的左右端点为a,b
res = max(0, (min(r, b)-max(l,a) + 1) // 右端点的最小值 - 左端点的最大值 + 1,与0取max是因为有可能没交集
常用位运算


找树根
for (int i = 0; i < n - 1; i++) // 输入n-1条边,构建树结构
{
int a, b;
cin >> a >> b; // 输入边的两个节点
add(b, a); // 添加从b到a的边,表示b是a的父节点
st[a] = true; // 标记a有父节点
}
int root = 1;
while (st[root]) root++; // 找到没有父节点的节点,即为根节点
向上取整
// 公式
int t = (x + k - 1) / k;
// 举例:
// 如果是除2向上取整
int t = (x + 2 - 1) / 2;
// 如果是除3向上取整
int t = (x + 3 - 1) / 3;
运算符重载
// 结构体+
Point operator+(Point a, Point b)
{
Point c;
c.x = a.x + b.x;
c.y = a.y + b.y;
return c;
}
// 结构体-
Point operator-(Point a, Point b)
{
Point c;
c.x = a.x - b.x;
c.y = a.y - b.y;
return c;
}
// 结构体*
Point operator*(Point a, int b)
{
Point c;
c.x = a.x * b;
c.y = a.y * b;
return c;
}
计算位置不同的元素的最小交换次数
memset(vis, 0, sizeof(vis)); // 重置访问标记
int cycles = 0; // 环的数量
// 计算当前排列的环数量
for (int i = 1; i <= n; i++)
{
if (!vis[i])
{
cycles++;
int j = i;
while (!vis[j])
{
vis[j] = true;
j = a[j]; // 从公司i开始,沿着排列找到下一个公司
}
}
}
// 最小交换次数 = n - 环的数量
int min_swaps = n - cycles;
生成不同排列
// a[]为{1, 2, 3, 4};
do
{
// 会打印 1 2 3 4 / 1 2 4 3 / ... 的不同排列
for (int i=1; i<=n; i++)
cout << a[i] << " ";
cout << endl;
} while (next_permutation(a+1, a+n+1));
找循环节
字符映射(含循环节)
// mp保存的是已经输入的字符映射关系
for (int ch = 'a'; ch <= 'z'; ch++) {
memset(vis, 0, sizeof(vis));
char cur = ch, tmp = cur;
while (true)
{
vis[cur] = true;
tmp = mp[tmp];
if (vis[tmp]) break;
cur = tmp;
}
mp[ch] = tmp;
}
模拟移动寻找循环
int cur = pow_new[s]; // 当前位置
int time = 0, cnt = 0; // time: 当前时间,cnt: 访问过的位置数量
memset(vis_time, -1, sizeof(vis_time)); // 初始化访问时间为-1
while (vis_time[cur] == -1) // 如果当前位置未被访问过
{
vis_time[cur] = time; // 记录访问时间
vis[cnt++] = cur; // 记录访问序列
cur = nxt_pos[cur]; // 移动到下一个位置
time++; // 时间增加
}
int cycle_start = vis_time[cur]; // 循环开始的时间
int cycle_length = time - cycle_start; // 循环长度
// 计算第q步后的位置
if (q < cnt) // 如果在循环开始之前
{
res_pos = vis[q];
}
else // 如果进入循环
{
int pos_in_cycle = (q - cycle_start) % cycle_length; // 在循环中的位置
res_pos = vis[cycle_start + pos_in_cycle];
}
cout << a[res_pos].id << endl; // 输出结果编号
题解:AtCoder AT_awc0025_d Constellation Observation Tour
DFS+回溯寻找循环
// 使用DFS+回溯求解任意点开始的环的长度
void dfs(int u)
{
// 如果已经计算过,直接返回
if (ans[u])
{
return;
}
// 如果访问到已标记的节点,说明找到了环
if (vis[u])
{
int len = 1;
int v = t[u];
// 计算环的长度
while (v != u)
{
len++;
v = t[v];
}
// 为环上所有节点设置结果
ans[u] = len;
v = t[u];
while (v != u)
{
ans[v] = len;
v = t[v];
}
return;
}
// 标记当前节点已访问
vis[u] = true;
dfs(t[u]); // 递归探索下一个节点
vis[u] = false; // 回溯,取消标记
// 如果当前节点的结果还未计算,则继承下一个节点的结果
if (!ans[u])
{
ans[u] = ans[t[u]];
}
}
题解:AtCoder AT_awc0030_d Telephone Game of Messages
数据超int范围时不可达判断
// 可使用INF=1e18来赋初值
for (int i=1; i<=n; i++)
dist[i] = INF;
if (dist[n]>=INF/2) // 判断>=INF/2即可
cout << -1;
计算某个区间 最大值-最小值=k 的区间对数量
// 分别计算<=k的区间对数量和<=k-1的区间对数量
ans = calc(k) - calc(k-1)
题解:AtCoder AT_awc0026_e Just the Right Temperature Difference
数字序列转字符串后的字符串截取
// 5 5 6 11 4 4 2 7 7 12,这样的数字序列转为字符串后,求从i截取到x个数字序列的字符串
// 预处理字符串前缀和长度
for (int i=1; i<=n; i++)
sa[i] = sa[i-1] + a[i].size();
// 计算第i个到第x个长度的字符串
for (int i=1; i+x-1<=n; i++)
{
int st_pos = sa[i]-a[i].size()+1;
int ed_pos = sa[i+x-1];
string tmp = s.substr(st_pos, ed_pos-st_pos+1); // s字符串下标从1开始
}
题解:洛谷 P2852 [USACO06DEC] Milk Patterns G
约束传递
// 求数组中相邻元素大小之差的绝对值的最小值,要求每个数只能增加不能减少
l[i] = max(l[i-1]-k, a[i])
r[i] = max(r[i+1]-k, a[i])
b[i] = max(l[i], r[i])
ans += b[i]-a[i]
题解:AtCoder AT_awc0005_c Staircase-Shaped Flower Bed
在一条路径上找最远的父节点
for (int i=2; i<=n; i++)
{
int fa = 1;
for (int j=1; j<=n; j++)
{
if (i==j) continue;
if (a[1][i]==a[1][j]+a[j][i] && a[1][j]>a[1][fa]) fa = j;
}
int w = a[1][i] - a[i][fa]; // fa为父节点
}
题解:洛谷 AT_abc451_e [ABC451E] Tree Distance
A - 基础算法
【高精度】
高精度加法
//高精度数输入
void s2BIG(string s, int a[])
{
a[0] = s.size();
for (int i=1; i<=a[0]; i++)
a[i] = s[a[0]-i] - '0';
}
// 高精度数输出
void printBIG(int a[])
{
for (int i=a[0]; i>=1; i--)
cout << a[i];
cout << endl;
}
//低精度转高精度
void i2BIG(int n, int a[])
{
while (n>0)
{
a[++a[0]] = n % 10;
n /= 10;
}
if (a[0] == 0) a[0] = 1;
}
//高精加
void addBIG(int a[], int b[], int c[])
{
c[0] = max(a[0], b[0]);
int u = 0; // u存储低位向高位的进位
for (int i=1; i<=c[0]; i++)
{
int t = a[i] + b[i] + u;
c[i] = t % 10;
u = t / 10;
}
if (u>0) c[++c[0]] = u;
}
高精度减法
//高精减
void subBIG(int a[], int b[], int c[])
{
c[0] = max(a[0], b[0]);
int u = 0;
for (int i=1; i<=c[0]; i++)
{
int t = a[i]-b[i]-u;
if (t<0)
{
u = 1;
c[i] = t + 10;
}
else
{
u = 0;
c[i] = t;
}
}
while (c[c[0]] == 0 && c[0]>1) c[0]--;
}
高精度乘法
//高精乘(高精*低精)
void mulBIG(int a[], int b, int c[])
{
c[0] = a[0];
int u = 0;
for (int i=1; i<=c[0]; i++)
{
int t = a[i] * b + u;
c[i] = t % 10;
u = t / 10;
}
while (u>0)
{
c[++c[0]] = u % 10;
u /= 10;
}
}
//高精乘(高精*高精)
void mulBIG2(int a[], int b[], int c[])
{
c[0] = a[0] + b[0];
int u = 0;
for (int j=1; j<=b[0]; j++)
{
int u = 0;
for (int i=1; i<=a[0]; i++)
{
int t = a[i]*b[j] + c[i+j-1] + u;
c[i+j-1] = t % 10;
u = t / 10;
}
if (u>0)
c[a[0]+j] += u;
}
while (c[0]>1 && c[c[0]]==0)
{
c[0]--;
}
}
高精度除法
//高精度除法
// 高精除低精
void divBIG(int a[], int b, int c[])
{
c[0] = a[0];
int r = 0;
for (int i=c[0]; i>=1; i--)
{
int t = r * 10 + a[i];
c[i] = t / b;
r = t % b;
}
while (c[c[0]] == 0 && (c[0]>1)) c[0]--;
}
// 高精除高精
void divBIG2(int a[], int b[], int c[])
{ // 将高精度数字a[]与高精度数字b[]相除,所得的商存入高精度数组c[]中
// int temp[1005] = {0}; // 临时数组
// int count[1005] = {0}; // 计数数组
c[0] = 1; // 初始商为0
c[1] = 0;
// 特殊情况处理
if(cmpBIG(a, b))
{ // 如果a < b,直接返回0
c[0] = 1;
c[1] = 0;
return;
}
// 计算商的位数
c[0] = a[0] - b[0] + 1;
for(int i = 1; i <= c[0]; i++) c[i] = 0;
// 模拟竖式除法
for(int i = c[0]; i >= 1; i--)
{
// 调整除数位数
int tempB[1005] = {0};
tempB[0] = b[0] + i - 1;
for(int j = 1; j <= b[0]; j++)
tempB[j+i-1] = b[j];
// 试商
while(!cmpBIG(a, tempB))
{
// a = a - tempB
subBIG(a, tempB, a);
c[i]++; // 商对应位加1
// 处理进位
int j = i;
while(c[j] > 9)
{
c[j+1] += c[j] / 10;
c[j] %= 10;
j++;
if(j > c[0]) c[0] = j;
}
}
}
// 去除前导零
while(c[c[0]] == 0 && c[0] > 1) c[0]--;
}
高精度求余
int mod(string s, int div)
{
int rem = 0;
for (int i=0; i<s.size(); i++)
{
rem = (rem*10+(s[i]-'0')) % div;
}
return rem;
}
//如果rem为0说明是s是div的倍数
高精度封装
// 高精度结构体
struct BIG
{
int len, num[1005];
BIG()
{// 构造函数:将高精度数字初始化为0
memset(num, 0, sizeof(num));
len = 1;
}
void set(int x)
{// 将int类型数字x转化为高精度数字
memset(num, 0, sizeof(num));
len = 0;
while (x>0)
{
num[++len] = x % 10;
x /= 10;
}
if (len==0) len = 1; // 特判0的情况
}
void set(string s)
{// 将string类型转化为高精度数字
memset(num, 0, sizeof(num));
len = s.size();
for (int i=1; i<=len; i++)
num[i] = s[len-i] - '0';
}
void print()
{// 输出高精度数字
for (int i=len; i>=1; i--)
cout << num[i];
cout << endl;
}
string toS() // 将高精度数存成字符串,用法 string t = A.toS();
{
for (int i=len; i>=1; i--)
t += to_string(num[i]);
return t;
}
};
// 高精度加法封装
BIG operator+(BIG a, BIG b)
{
BIG c;
c.len = max(a.len, b.len);
int u = 0;
for (int i=1; i<=c.len; i++)
{
int t = a.num[i] + b.num[i] + u;
c.num[i] = t % 10;
u = t / 10;
}
if (u>0) c.num[++c.len] = u;
return c;
}
// 高精度减法封装
bool operator<(BIG a, BIG b)
{
if (a.len != b.len) return a.len < b.len;
for (int i=a.len; i>=1; i--)
if (a.num[i] != b.num[i])
return a.num[i] < b.num[i];
return false;
}
bool operator==(BIG a, BIG b)
{
if (a.len != b.len) return false ;
for (int i=a.len; i>=1; i--)
if (a.num[i] != b.num[i])
return false;
return true;
}
BIG operator-(BIG a, BIG b)
{
BIG c;
c.len = max(a.len, b.len);
int u = 0;
for (int i=1; i<=c.len; i++)
{
int t = a.num[i] - b.num[i] - u;
if (t<0)
{
c.num[i] = t + 10;
u = 1;
}
else
{
c.num[i] = t;
u = 0;
}
}
while (c.num[c.len] == 0 && c.len>1) c.len--;
return c;
}
// 高精度乘法封装
BIG operator*(BIG a, int b)
{
BIG c;
c.len = a.len;
int u = 0;
for (int i=1; i<=c.len; i++)
{
int t = a.num[i] * b + u;
c.num[i] = t % 10;
u = t / 10;
}
while (u>0)
{
c.num[++c.len] = u % 10;
u /= 10;
}
return c;
}
// 高精度除法封装
BIG operator/(BIG a, int b)
{
BIG c;
c.len = a.len;
int r = 0;
for (int i=c.len; i>=1; i--)
{
int t = r * 10 + a.num[i];
c.num[i] = t / b;
r = t % b;
}
while (c.num[c.len] == 0 && c.len > 1) c.len--;
return c;
}
【整数二分】
二分查找
// lower_bound(): 返回第一个 >= x 的位置
// upper_bound(): 返回第一个 > x 的位置
// upper_bound()-1: 返回最后一个 <= x 的位置
// upper_bound()-lower_bound(): 等于x的个数
二分答案
// [l,r]区间寻找最小的最大值
int l = 0, r = 1e18;
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
// l就是答案
// [l, r]区间寻找最大的最小值
while (l < r)
{
int mid = l + r + 1 >> 1; //当l=mid时,一定要l+r+1
if (check(mid)) l = mid;
else r = mid - 1;
}
// l就是答案
二分分段数
// 寻找最大的最小值时,确认分段数是否>=k
bool check(int x)
{
for (int i=1; i<=n; i++)
{
if (sum+a[i]>=x)
{
ans++;
sum = 0;
}
else sum += a[i];
}
return ans >= k;
}
【浮点二分】
bool check(double x) {/* ... */} // 检查x是否满足某种性质,注意如果求等于某值,那这里判断要为小于或大于,不能像整数二分一样为等于
double bsearch_3(double l, double r)
{
double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求,需要比预留的小数位+2
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
【前缀和】
一维前缀和
// 前缀和求解
for (int i=1; i<=n; i++) s[i] = s[i-1] + a[i];
printf("%d\n", s[r]-s[l-1]);
// 可用于连续子数组查找的优化,例如查找有多少的[L,R]满足区间和为k,即查找以R结尾的数组中有多少个
sum[L-1] == sum[R]-k。
// 快速求数组中所有数字的曼哈顿距离之和
// 1 4 7,曼哈顿距离和 = 3 + 6 + 3 = 12
for (int i=1; i<=n; i++)
ans += a[i]*i - sa[i];
二维前缀和
//预处理s[i,j]
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] + a[i][j];
//询问
while (q--) {
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);
}
【差分】
一维差分基本操作
// 获得差分数组
for (int i=1; i<=n; i++) b[i] = a[i]-a[i-1];
// for (int i=1; i<=n; i++) insert(i, i, a[i]);
// 给区间[l, r]中的每个数加上c:
void insert(int l, int r, int c)
{
b[l]+=c;
b[r+1]-=c;
}
// 还原差分数组为原数组
for (int i=1; i<=n; i++) b[i] += b[i-1];
0/1之间状态切换(开关问题)
// 状态在0/1之间切换,某个位置操作会影响一个区间
for (int i=1; i<=n; i++)
{
diff[i] = (diff[i-1] + diff[i])%2;
int state = a[i] ^ diff[i];
if (state ==1)
{
if (i+k-1<=n)
{
ans++;
diff[i] = (diff[i] + 1) % 2;
diff[i+k] = (diff[i+k] - 1 + 2) % 2;
}
else
{
cout << -1 << endl;
return 0;
}
}
}
题解:AtCoder AT_awc0022_d Simultaneous Control of Light Bulb Panels
区间修改操作,问多少区间满足操作次数>=k
// 区间长度达到10^9,针对区间进行修改操作,问多少区间满足操作系统>=k
for (int i = 1; i <= n; i++) // 处理每个区间
{
int l, r;
cin >> l >> r; // 读入区间左端点和右端点
mp[l]++; // 在l处进入区间,覆盖次数+1
mp[r]--; // 在r处离开区间,覆盖次数-1
}
int last = 0, cnt = 0; // last: 上一个坐标点,cnt: 当前覆盖次数
// 扫描线遍历所有关键点
for (auto x : mp) // 按坐标从小到大遍历
{
int pos = x.first; // 当前坐标
int delta = x.second; // 覆盖次数的变化量
if (cnt >= k) // 如果上一个区间的覆盖次数≥k
{
// 累加上一个区间[last, pos)的长度
ans += pos - last;
}
last = pos; // 更新上一个坐标
cnt += delta; // 更新当前覆盖次数
}
题解:AtCoder AT_awc0026_d Repainted Wall
让每个元素至少达到阈值
for (int i = 1; i <= n; i++) // 遍历每个位置
{
cur += imos[i]; // 更新当前位置的累计增加值
if (a[i] + cur < t) // 如果当前值小于目标值
{
int need = t - (a[i] + cur); // 计算需要增加的值
ans += need; // 累计操作次数
if (i + k <= n) // 如果影响范围未超出数组
{
imos[i + k] -= need; // 在差分数组记录减少
}
cur += need; // 当前累计值增加
}
}
题解:AtCoder AT_awc0034_c Watering the Flower Bed
二维差分基本操作
//预处理出b[i][j]差分数组
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
insert(i, j, i, j, a[i][j]);
// 给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
void insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
}
//通过前缀和还原数组
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
b[i][j] += b[i-1][j] + b[i][j-1] - b[i-1][j-1];
【ST表】
// 预处理ST表
for (int i=1; i<=n; i++) scanf("%d", &f[i][0]);
for (int j=1; j<=20; j++)
for (int i=1; i+(1<<j)-1<=n; i++)
f[i][j] = max(f[i][j-1], f[i+(1<<(j-1))][j-1];
for (int i=1, l, r; i<=m; i++)
{
scanf("%d%d", &l, &r);
int k = log2(r-l+1);
printf("%d\n", max(f[l][k], f[r-(1<<k)+1][k]);
}
【快速排序】
// AcWing模板
// do-while版本
void quick_sort(int q[], int l, int r)
{
if (l>=r) return;
int rnd_idx = rand() % (r-l+1)+l;
swap(q[l], q[rnd_idx]);
int x=q[l], i=l-1, j=r+1;
while (i<j)
{
do i++; while (q[i]<x);
do j--; while (q[j]>x);
if (i<j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j+1, r);
}
// while版本
void quick_sort(int q[], int l, int r)
{
if (l>=r) return;
int rnd_idx = rand() % (r-l+1)+l;
swap(q[l], q[rnd_idx]);
int x=q[l], i=l-1, j=r+1;
while (i<j)
{
while (q[++i]<x);
while (q[--j]>x);
if (i<j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j+1, r);
}
void qsort(int l, int r)
{ // 对区间[l, r]进行快速排序
int x = a[(l+r)/2], i=l, j = r; // 选择中间的元素作为基准值
while (i<=j)
{
while (a[i]<x) i++; // 在x左边从左到右找到第一个>=x的数字a[i]
while (a[j]>x) j--; // 在x右边从右到左找到第一个<=x的数字a[j]
// 如果找到的两个元素的下标没有交错,交换这两个元素,并移动下标
if (i<=j) swap(a[i], a[j]), i++, j--;
}
if (l<j) qsort(l, j); // 递归地对左半部分进行快速排序
if (i<r) qsort(i, r); // 递归地对右半部分进行快速排序
}
【归并排序】
// AcWing模板
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
else tmp[k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
void merge_sort(int l, int r)
{
if (l==r) return; // 如果子数组只有一个元素,则不需要排序,直接返回
int mid = (l + r) / 2 ;
merge_sort(l, mid); // 递归地对左半部分进行归并排序
merge_sort(mid+1, r); // 递归地对右半部分进行归并排序
int x = l, y = mid+1, cnt = l-1;
while (x<=mid && y<=r) // 只要左右两端子数组都不为空
{
if (a[x]<=a[y]) // 将两段子数组左端点较小者插入b数组
{
b[++cnt] = a[x];
x++;
}
else
{
b[++cnt] = a[y];
y++;
}
}
// 合并过程中其中一个子数组的元素已全部合并完成,而另一个子数组仍有剩余元素的情况
for (int i=x; i<=mid; i++) b[++cnt] = a[i]; // 如果左半部分还有剩余的元素,将它们复制到b中
for (int i=y; i<=r; i++) b[++cnt] = a[i]; // 如果右半部分还有剩余的元素,将它们复制到b中
for (int i=l; i<=r; i++) a[i] = b[i]; // 将b中已经排序好的部分复制回a中,完成合并
}
【堆排序】
基本声明方式
// 整型大根堆
priority_queue<int, vector<int> > pq;
// 整型小根堆
priority_queue<int, vector<int>, greater<int> > pq;
// 结构体中符号重载,< 表示大的在前,> 表示小的在前(对应greater)
struct Node
{
int val;
// 自定义符号重载,< 表示大的在前,> 表示小的在前(对应greater)
// bool operator<(const Node& other) const {
// return val < other.val; // 大的在前
// }
};
// 结构体类型大根堆
priority_queue<Node, vector<Node> > pq;
// 在结构体外定义符号重载,< 表示大的在前,> 表示小的在前(对应greater)
bool operator<(Node a, Node b) // 表示大的在前
{
return a.val < b.val;
}
核心操作
// 插入元素
pq.push(x);
// 删除元素
pq.pop();
// 取队首元素
int top = pq.top();
// 判断堆是否为空
if (pq.empty()) { ... }
// 取堆的大小
int size = pq.size();
双堆技巧
// 应用场景:动态维护数据流的中位数
// 大根堆存储较小的一半,小根堆存储较大的一半
priority_queue<int> small;
priority_queue<int, vector<int>, greater<int> > bigg;
// 若元素为偶数个,smaller与bigger元素个数相等
// 若元素为奇数个,smaller比bigger元素多一,则small的队首即为中位数
if(smaller.size() > bigger.size() + 1)
{
int tmp = smaller.top();
smaller.pop();
bigger.push(tmp);
sumSmall -= tmp;
sumBig += tmp;
}
else if(smaller.size() < bigger.size())
{
int tmp = bigger.top();
bigger.pop();
smaller.push(tmp);
sumSmall += tmp;
sumBig -= tmp;
}
// 如果还要计算中位数到所有元素的距离和,需要维护一个变量sumSmall和sumBig,分别表示small和bigger中元素的和
int leftSum = x * (int)small.size() - sumSmall; // small中元素到x的距离和
int rightSum = sumBig - x * (int)big.size(); // big中元素到x的距离和
int res = leftSum + rightSum;
修改堆中数据
// 应用场景:修改优先队列中元素的值
// 可以定义一个数组用于离线处理,当堆中数据访问到的时候(pq.top()),再根据数组中的标志位来修改或删除元素
自定义堆实现
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], hsize;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u*2<=hsize && h[u*2]<h[t]) t = u*2;
if (u*2+1<= size && h[u*2+1]<h[t]) t = u*2+1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u/2>=1 && h[u]<h[u/2])
{
heap_swap(u, u / 2);
u /= 2;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
【双指针】
基本声明方式(for+while)
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ; // 这里while的内容也跟着题目要求变化
// 具体问题的逻辑
}
/*
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
(3) 对于两个序列,判断一个序列是否为另一个序列的子序列
*/
先移动j至合适位置,然后满足则i,j同时移动,不满足移动i
// 先移动j至合适位置,然后满足则i,j同时移动,不满足移动i
int i=1, j=1;
while (i<=n && j<=m)
{
// 跳过所有金额小于当前商品价格的优惠券
while (j <= m && r[j] < c[i])
{
j++;
}
// 如果还有可用的优惠券
if (j <= m)
{
i++;
j++; // 使用当前优惠券
cnt++; // 成功匹配一个商品
}
else
i++;
}
题解:AtCoder AT_awc0002_d Keys and Treasure Boxes
满足则i和j同时移动,不满足则移动j
// 满足则i和j同时移动,不满足则移动j
int i = 1, j = 1; // i: 指向b数组的指针, j: 指向a数组的指针
// 使用双指针贪心匹配
while (i <= m && j <= n)
{
if (a[j] >= b[i]) // 如果当前a元素 >= 当前b元素,可以匹配
{
ans++; // 匹配数量加1
i++; // 移动到下一个b元素
j++; // 移动到下一个a元素
}
else // 当前a元素 < 当前b元素,a元素太小,看下一个a元素
{
j++; // 移动到下一个a元素
}
}
题解:AtCoder AT_awc0015_d Assignment of Cooking Tasks
满足则i移动,不满足则j移动
// 满足则i移动,不满足则j移动
int i = 1, j = 1; // 双指针,i: 左边界,j: 右边界
while (i <= n && j <= n)
{
if (sa[j] - sa[i - 1] > k) // 区间和超过k
{
f[i] = j; // 记录以i为左边界,区间和刚好超过k的右边界j
i++; // 左边界右移
}
else if (j == n) // 右边界已到数组末尾
{
f[i] = j; // 记录以i为左边界的右边界为n
i++; // 左边界右移
}
else
{
j++; // 右边界右移
}
}
题解:AtCoder AT_awc0016_d The Path of Carrying Luggage
满足条件则j移动,不满足条件则i移动
// 满足条件则j移动,不满足条件则i移动
int l = 1, r = 1; // 双指针,表示当前区间[l, r]
while (l <= n && r <= n) // 双指针遍历数组
{
// 将a[r]加入最小值队列(维护单调递增)
while (!dqmin.empty() && dqmin.back().v >= a[r])
{
dqmin.pop_back(); // 移除队尾比a[r]大的元素
}
dqmin.push_back({a[r], r}); // 将a[r]加入队列
// 将a[r]加入最大值队列(维护单调递减)
while (!dqmax.empty() && dqmax.back().v <= a[r])
{
dqmax.pop_back(); // 移除队尾比a[r]小的元素
}
dqmax.push_back({a[r], r}); // 将a[r]加入队列
// 调整左指针l,使当前区间[l, r]的最大值-最小值<=k
while (!dqmin.empty() && !dqmax.empty() && dqmax.front().v - dqmin.front().v > k)
{
// 如果队首元素是左指针l指向的元素,则从队列中移除
if (dqmax.front().idx == l)
{
dqmax.pop_front(); // 从最大值队列中移除过期的元素
}
if (dqmin.front().idx == l)
{
dqmin.pop_front(); // 从最小值队列中移除过期的元素
}
l++; // 左指针右移,缩小区间
}
r++; // 右指针右移,扩展区间
// 统计以r-1为右端点的满足条件的区间数量
// 区间[l, r-1]满足条件,所以有(r-1)-l+1 = r-l+1个区间
res += r - l + 1;
}
题解:AtCoder AT_awc0026_e Just the Right Temperature Difference
满足则i和j同时移动,不满足则重新枚举起始位置
int st = 1; // 起始位置
// 遍历所有可能的起始位置
while (st <= n - m + 1) {
int i = st, j = 1;
// 尝试匹配
while (j <= m && a[i] == b[j]) {
i++;
j++;
}
// 如果b的所有元素都匹配成功
if (j > m) {
cout << st << endl; // 输出起始位置
return 0;
}
st++; // 尝试下一个起始位置
}
// 没有找到匹配
cout << -1 << endl;
题解:AtCoder AT_awc0045_b Order of Books on the Bookshelf
【贪心】
任务安排
// 区间 [l,r] 贪心,为解决尽可能安排所有的任务(每个任务都需在[l,r]内寻找一个可行的任务)
bool cmp(Node x, Node y)
{
if (x.r != y.r) // 如果右端点不同
return x.r < y.r; // 按右端点升序排序
return x.l < y.l; // 右端点相同时,按左端点升序排序
}
题解:AtCoder AT_awc0004_d Parking Lot Assignment
区间合并
// 将所有存在交集的区间合并
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg: segs)
{
if (ed < seg.first)
{
if (st!=-2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else
{
ed = max(ed, seg.second);
}
}
if (st!=-2e9) res.push_back({st, ed});
segs = res;
}
区间覆盖
// [1,n]中寻找最少区间覆盖
int r = 0;
for (int i=1; i<=m && r<n; i++)
{
if (a[i].l > r + 1)
{
cout << -1 << endl;
return 0;
}
int maxr = 0, maxi = i;
for (int j=i; j<=m && a[j].l<=r+1; j++)
{
if (a[j].r>maxr)
{
maxr = a[j].r;
maxi = j;
}
}
if (maxr<=r)
{
cout << -1 << endl;
return 0;
}
r = maxr;
cnt++;
i = maxi;
}
if (r>=n) cout << cnt;
else cout << -1 << endl;
// [s,t]中寻找最少区间覆盖
int r=s;
for (int i=1; i<=n && r<t; i++)
{
if (ls[i].a > r + 1)
{
cout << "impossible" << endl;
return 0;
}
int maxr = 0, maxi = i;
for (int j=i; j<=n && ls[j].a<=r; j++)
{
if (ls[j].b>maxr)
{
maxr = ls[j].b;
maxi = j;
}
}
if (maxr<=r)
{
cout << "impossible" << endl;
return 0;
}
r = maxr;
cnt++;
i = maxi;
}
if (r>=t) cout<<cnt;
else cout<<"impossible";
// 确认[0,L]区间是否全覆盖
for (int i = 1; i <= n && r < l; i++)
{
// 检查是否有间隙:如果当前路由器左边界 > 已覆盖最右位置+1,说明有覆盖不到的区域
if (a[i].l > r + 1)
{
cout << "No" << endl;
return 0;
}
int maxr = 0, maxi = i; // maxr: 能找到的最大右边界, maxi: 对应路由器的索引
// 在左边界 <= 当前已覆盖位置的路由器中,找到右边界最大的
for (int j = i; j <= n && a[j].l <= r; j++)
{
if (a[j].r > maxr)
{
maxr = a[j].r; // 更新最大右边界
maxi = j; // 记录对应的路由器索引
}
}
// 如果最大右边界 <= 当前已覆盖位置,说明无法继续扩展
if (maxr <= r)
{
cout << "No" << endl;
return 0;
}
r = maxr; // 更新已覆盖的最右位置
i = maxi; // 跳转到具有最大右边界的路由器,继续处理
}
题解:AtCoder AT_awc0006_d Placement of Security Guards
添加更短的任务(更轻的箱子)
// 给定箱子的重量和承重,叠放每个箱子,要求下面的箱子可以承受上面所有箱子的重量
// 按照重量+承重,从小到大排序
bool cmp(Node x, Node y)
{
return x.d + x.w< y.d + y.w;
}
sort(a+1, a+n+1, cmp);
for (int i = 1; i <= n; i++) // 遍历所有箱子
{
if (a[i].d >= tot) // 如果当前箱子的承重大于等于所有叠放箱子的重量
{
pq.push(a[i].w); // 将箱子加入堆
tot += a[i].w; // 增加总重量
}
else // 否则
{
if (a[i].w < pq.top()) // 如果当前箱子的重量小于堆中最大箱子的重量
{
tot += (a[i].w - pq.top()); // 更新总重量
pq.pop(); // 移除堆顶箱子
pq.push(a[i].w); // 加入当前箱子
}
// 否则,不选择当前箱子
}
}
题解:AtCoder AT_awc0014_a Loading Cargo
检查二分图多重匹配是否存在
sort(l + 1, l + n + 1); // 对学生能力排序
sort(r + 1, r + m + 1, greater<int>()); // 对老师要求降序排序
int suml = 0, sumr = 0; // suml: 学生总能力, sumr: 老师总要求
for (int i = 1; i <= m; i++) // 遍历老师
{
sumr += r[i]; // 累加老师要求
// 查找第一个能力 >= i 的学生位置
int pos = lower_bound(l + 1, l + n + 1, i) - l;
// 能力 >= i 的学生数量
suml += (n - (pos - 1));
cout << "sumr suml " << sumr << " " << suml << endl;
// 如果当前学生的总能力小于老师总要求
if (suml < sumr)
{
cout << "No" << endl;
return 0;
}
}
题解:AtCoder AT_awc0031_d Library Inventory Check
哈夫曼编码模型
// 应用:合并果子问题
priority_queue<int, vector<int>, greater<int>> pq;
for (int i = 1; i <= n; i++) {
pq.push(a[i]);
}
int sum = 0;
for (int i = 1; i < n; i++) {
int x1 = pq.top(); pq.pop();
int x2 = pq.top(); pq.pop();
sum += x1 + x2;
pq.push(x1 + x2);
}
关灯问题
// 0表示关着灯,1表示开着灯,如果出现 1 0 1,那么0就是被打扰的,问最少关掉多少房间的灯
// 贪心策略:如果消去左边的1,那么不会对后面的序列造成任何影响,所以每次发现1 0 1序列时将其转化为1 0 0
for (int i=2; i<n; i++)
if (a[i-1] && a[i]==0 && a[i+1])
{
a[i+1] = 0;
cnt++;
}
最大子序列和问题
// 每次操作可以讲子序列中的数都改为他们的平均数,问最多可以使多少个数大于等于x
// 贪心策略:从大到小枚举数字a[i],找到最后一个前i项和>=ans*x的项数i
for (int i=1; i<=n; i++)
{
tot += a[i];
if (tot/i>=x) continue;
else
{
cout << i-1 << endl;
flag = 0;
break;
}
}
if (flag) cout << n << endl;
最多得分问题
// 玩家A攻击怪物得1分,玩家B攻击怪物得0分,每次都是A先攻击。A还可以迫使B放弃一次攻击,可使用k次,问A最多得到多少分
// 贪心策略
// 1. 剩余体力值r的伤害次数中只有第一次是A的轮次攻击,后面都得使用特殊能力使B放弃攻击,所以特殊能力的使用次数为(r-1)/a
// 2. 将所有怪物按照A使用特殊能力的次数从小到大排序,优先使用特殊能力次数少的怪物
for (int i=1; i<=n; i++)
{
cin >> r[i];
int r;
if (c[i]%(a+b)==0) r = a+b;
else r = c[i]%(a+b);
if (r%a==0) d[i] = (r-1)/a;
else d[i] = r/a;
}
sort(d+1, d+n+);
叠罗汉问题
// 奶牛有自己的重量和力量,彼此叠罗汉,确定顺序,使风险中的最大值尽可能小
// 贪心策略:将所有奶牛按照重量+力量的和从小到大排序
bool cmp(Node x, Node y)
{
return x.w+x.s < y.w+y.s;
}
字符串连接问题
// n个字符串,只包含s和h,问如何将这些字符串连起来,使得字符串中子序列sh的个数最多
// 贪心策略:将所有字符串按 s个数/h个数 从大到小排序
bool cmp(Node x, Node y)
{
return x.s * y.h > y.s * x.h;
}
建货仓问题
// 建货仓,使得与每家商店的距离和最小
// 贪心策略:x为中位数,x1和xn中间的数,
sort(a+1, a+n+1);
for (int i=1; i<=(n+1)/2; i++)
ans += a[n-i+1] - a[i];
// 建货仓,距离d公里的运费是W*d,问建在何处,使得运费之和最小
// 贪心策略:
// 1. 按照d从小到大排序
// 2. 枚举i,当前i项的运费和大于一半时,货仓地址就是在d[i]其中
bool cmp(node x,node y)
{
return x.d<y.d;
}
for (int i=1;i<=n;i++)
{
cin>>a[i].w;
sum+=a[i].w*a[i].d;
}
sort(a+1,a+n+1,cmp);
int tot=0;
for (int i=1;i<=n;i++)
{
tot+=a[i].w*a[i].d;
if (tot>=sum-tot)
{
int pos=a[i].d;
for (int j=1;j<=n;j++)
ans+=abs(a[j].d-pos)*a[j].w;
break;
}
}
奶牛吃草
// 比较函数:按t[i]/d[i]从大到小排序
bool cmp(Node x, Node y)
{
return x.t * y.d > x.d * y.t; // 等价于x.t/x.d > y.t/y.d,避免浮点数
}
题解:洛谷 P2878 [USACO07JAN] Protecting the Flowers S
过河问题
sort(t + 1, t + n + 1); // 按过河时间从小到大排序
for (idx = n; idx > 3; idx -= 2) // 每次送最慢的两个人过河
{
int p_1 = t[2] + t[1] + t[idx] + t[2]; // 方案1
int p_2 = t[idx - 1] + t[1] + t[idx] + t[1]; // 方案2
ans += min(p_1, p_2); // 选择时间短的方案
}
if (idx == 2) // 如果还剩2个人
{
ans += t[2]; // 最快的两人一起过
}
else // 如果还剩3个人
{
ans += t[1] + t[2] + t[3]; // 特殊处理
}
购买牛奶最小成本(二进制优化)
ans = 1e18; // 初始答案
sum = 0; // 当前花费
for (int i = min(31LL, n); i >= 1; i--) // 从大到小处理
{
int base = pow(2, i - 1); // 2^(i-1)
sum += a[i] * (x / base); // 用第i个物品填充
int t = sum;
if (x % base) // 如果还有剩余
{
t += a[i]; // 多买一个
}
ans = min(ans, t); // 更新最小花费
x %= base; // 更新剩余需求
}
题解:洛谷 P15256 [USACO26JAN2] Purchasing Milk B
环形数列移动至平均值的最小次数
// 贪心传递,使每个人达到平均数
for (int i = 1; i < n; i++)
{
int t = avg - a[i]; // 第i人需要调整的数量
a[i] = avg; // 第i人调整为平均数
a[i + 1] -= t; // 从下一个人处调整
b[i + 1] = t; // 记录传递量
ans += abs(t); // 累加操作次数
}
// 尝试正偏移
for (int i = 1;; i++)
{
int res = calc(i);
if (res > ans) // 如果操作次数增加,停止
{
break;
}
else
{
ans = res; // 更新最少操作次数
}
}
// 尝试负偏移
for (int i = 1;; i++)
{
int res = calc(-i);
if (res > ans)
{
break;
}
else
{
ans = res;
}
}
【位运算】
// 求n的第k位数字(0或1):
n >> k & 1
// 返回n的最后一位1:
lowbit(n) = n & -n
// 位运算求所有数位的组合(按顺序的组合)
for (int i=0; i<(1<<n); i++)
{
int x = 0;
for (int j=0; j<n; j++)
if ((i>>j) & 1)
x = x*10 + str[j]-'0';
}
// 例如数字8314,就会生成 0 8 3 83 1 81 31 831 4 84 ...(共16个)
【离散化】
结构体方式离散化
//对原有数组去重
struct Node
{
int val, pos, newval;
}a[N];
bool cmp(Node x, Node y)
{
return x.val < y.val;
}
bool cmp2(Node x, Node y)
{
return x.pos < y.pos;
}
for (int i=1; i<=n; i++)
{
cin >> a[i].val;
a[i].pos = i;
}
sort(a+1, a+n+1, cmp);
int cnt = 0;
for (int i=1; i<=n; i++)
{
if (a[i].val!=a[i-1].val)
cnt++;
a[i].newval = cnt;
}
sort(a+1, a+n+1, cmp2); // 根据pos,从小到大还原
// 以下为示例
/*
7
10 9 7 11 10 8 6
1 10 5
2 9 4
3 7 2
4 11 6
5 10 5
6 8 3
7 6 1
14
*/
vector方式离散化
vector<int> alls;
map<int, int> mp;
// 对alls添加数据(略)
// 离散化处理
vector<int> compressed = compress(alls);
// 建立映射关系:原值 -> 离散化后的索引
for (int i=0; i<alls.size(); i++)
mp[alls[i]] = compressed[i];
// 离散化函数:将原始值映射到1开始的连续整数
vector<int> compress(vector<int> a)
{
auto b = a; // 复制一份原始数据
sort(b.begin(), b.end()); // 排序
b.erase(unique(b.begin(), b.end()), b.end()); // 去重
vector<int> res(a.size()); // 创建结果数组,指定大小避免越界
for (int i = 0; i < a.size(); i++)
{
// 使用二分查找获取离散化后的索引(从1开始)
res[i] = lower_bound(b.begin(), b.end(), a[i]) - b.begin() + 1;
}
return res;
}
题解:AtCoder AT_awc0027_e Selection of Contiguous Intervals
B - 搜索
【DFS-一维】
void dfs(int u)
{
if (u==n)
{ // 到达n层后结束搜索
for (int i=0; i<n; i++)
{
// 按照题目要求执行,或者打印、计算等
}
return ; // 一定要加这句返回
}
// 注意:DFS没有通用模板,每道题都有自己的顺序,这里需要根据每道题进行修改
for (int i=1; i<=n; i++)
{ // 每个位置可以遍历的内容,如这里是可以填入1~n的数字
if (!st[i])
{ // 判断状态,确定是否可以填入
path[u] = i;
st[i] = true;
dfs(u+1); // DFS搜索
// path[u] = 0; // 可以不要这句,因为每次都被赋值
st[i] = false;
}
}
}
// 从第0层开始遍历
dfs(0);
// 剪枝方法1
// 增加sum参数,直接传递当前已选数的总和,避免重复计算,从而提高效率
void dfs(int step, int sum)
// 剪枝方法2
// 只要满足一种方案就退出
void dfs(int step)
{
if (flag) return;
}
【BFS-一维】
// 该模板存在问题是队列长度只增不减,可使用vector方式保持队列长度始终在范围内
typedef pair<int, int> PII; // 三个及以上的参数适合用结构体
PII q[N*N];
int d[N][N]; // 记录每个点到起点的距离
int dx[4]={-1,0,1,0};
int dy[4]={0,1,0,-1};
int bfs()
{
int hh=0, tt=0;
q[0] = {0, 0};
memset(d, -1, sizeof(d)); // 初始化每个点到起点的距离
d[0][0] = 0; // 起点的距离为0
while (hh<=tt)
{
auto t = q[hh++];
for (int i=0; i<4; i++)
{
int x = t.first+dx[i], y = t.second+dy[i];
if (x>=0 && x<n && y>=0 && y<m && g[x][y]==0 && d[x][y]==-1)
{
d[x][y] = d[t.first][t.second]+1;
q[++tt] = {x,y};
}
}
}
return d[n-1][m-1];
}
bfs() // 调用bfs(),返回最短路径
【BFS-二维】
// 用vector改写
typedef pair<int, int> PII; // 三个及以上的参数适合用结构体
queue<PII> q;
int d[N][N]; // 记录每个点到起点的距离
int dx[4]={-1,0,1,0};
int dy[4]={0,1,0,-1};
int bfs()
{
q.push({0,0});
memset(d, -1, sizeof(d)); // 初始化每个点到起点的距离
d[0][0] = 0; // 起点的距离为0
while (!q.empty())
{
auto t = q.front(); q.pop();
for (int i=0; i<4; i++)
{
int x = t.first+dx[i], y = t.second+dy[i];
if (x>=0 && x<n && y>=0 && y<m && g[x][y]==0 && d[x][y]==-1)
{
d[x][y] = d[t.first][t.second]+1;
q.push({x,y});
}
}
}
return d[n-1][m-1];
}
bfs() // 调用bfs(),返回最短路径
// 寻找最短路
void bfs()
{
q.push({1, 1, dist[1][1]})
while (!q.empty())
{
Node t = q.front();
int x = t.x, y = t.y, cost = t.cost;
q.pop();
for (int i=0; i<4; i++)
{
int nx = x + dx[i], ny = y + dy[i];
if (nx<1 || nx>n || ny<1 || ny>m) continue;
if (dist[nx][ny]> cost + 1)) // 这里可以根据题目要求修改
{
dist[nx][ny] = cost + 1;
q.push({nx, ny, dist[nx][ny]});
}
}
}
}
bfs()
// dist[n][m]为最短长度
折半搜索
void dfs(int pos, int end, ll sum, int type)
{
if (pos>end)
{
if (sum>S) return;
if (type==1) l[sum]++;
else r[sum]++;
return;
}
dfs(pos+1, end, sum, type);
dfs(pos+1, end, sum+a[pos], type);
}
unordered_map<ll, ll> l, r;
int mid = n/2;
dfs(1, mid, 0, 1);
dfs(mid+1, n, 0, 2);
ll ans = 0;
for (auto c : l)
{
ll left_sum = c.first, left_cnt = c.second;
if (r[S-left_sum])
ans += left_cnt * r[S-left_sum];
}
// ans就是方案数
题解:洛谷 P4799 [CEOI 2015] 世界冰球锦标赛 (Day2)
C - 数据结构
【队列】
基本操作
int q[N], hh, tt=-1;
//插入
q[++tt] = x;
//弹出
hh++;
//判断队列是否为空
if (hh<=tt) not empty
else empty
//取出队头元素
q[hh]
【链表】
list基本操作
(1) 获取起始迭代器: it = ls.begin(); // 指向第一个元素的首地址
(2) 获取结束迭代器: it = ls.end(); // 指向最后一个元素的尾地址
(3) 解引用访问数据: int v = *it; // 获取当前迭代器指向的元素值
(4) 向前/后遍历: it++或++it / it--或--it; // 移动到下/上一个节点
it = ls.end(), it--; // it指向名为ls的list的最后一个元素
(5) 移动操作:advance(it, n); // 将迭代器it向前或向后移动n个位置
next(it); // 返回it的下一个位置的迭代器(不修改原迭代器)
prev(it); // 返回it的上一个位置的迭代器(不修改原迭代器)
(6) 判断两个迭代器的关系:== / !=; // 判断两个迭代器是否指向同一元素
注意:链表迭代器不能比较大小
遍历方法一:用迭代器循环遍历链表;
for (list<int>:: iterator it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
迭代器类型:list<int>:: iterator是双向链表迭代器,支持++和--。
终止条件:it != ls.end() 确保遍历到最后一个有效元素后停止。
前缀递增:++it 或 it++ 即可,表示将迭代器指向下一个元素。
遍历方法二:先定义迭代器,再循环遍历;
list<int>:: iterator it;
for (it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
遍历方法三:使用auto自动推导迭代器类型来遍历链表;
for (auto it = ls.begin(); it != ls.end(); it++)
cout << *it << endl;
遍历方法四:范围循环——自动匹配元素类型,并自动遍历;
for (auto x : ls) cout << x << endl;
(7) ls.push_back(x) / ls.push_front(x): 在链表ls的尾部/头部插入一个元素x;
(8) ls.insert(): 在链表ls中插入元素,返回新插入元素的迭代器;
① ls.insert(it, y); // 在链表ls中it指向的位置之前插入元素y;
② ls.insert(it, x, y); // 在链表ls中it指向的位置之前插入x个元素y;
③ ls.insert(it, it1, it2); // 在链表ls中it指向的位置之前插入[it1, it2]之间的所有元素;
(9) ls.erase(it): 在链表ls中删除it所指向的元素;
若删除的元素在list的尾部,返回ls.end();
若删除的元素不再list的尾部,返回下一个元素的迭代器;
(10) ls.empty(): 判断链表ls是否为空;
(11) ls.clear(): 清空链表sl
(12) ls.size(): 返回链表ls中的元素个数;
(13) ls.remove(x): 在链表ls中删除所有等于x的元素;
(14) ls.reverse(): 反转链表ls中元素的顺序;
(15) ls.splice(): 将一个链表或子链表移动到当前链表的执行位置;
① 移动整个链表: s.splice(it, t); 将整个链表t都移动到链表s的指定位置it处;
② 移动单个元素: s.splice(it1, t, it2); 将链表t中it2指向的元素移动到链表s的指定位置it1处;
③ 移动子链表: s.splice(it1, t, it2, it3); 将链表t中[it2, it3)之间的元素移动到链表s的指定位置it1处;
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void add_to_head(int a)
{
e[idx] = a, ne[idx] = head, head = idx++;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}
// 将头结点删除,需要保证头结点存在
void remove_head()
{
head = ne[head];
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
//在下标是k的点的右边,插入x
void add(int k, int x)
{
e[idx] = x;
r[idx] = r[k], l[idx] = k;
l[r[k]] = idx, r[k] = idx++;
}
//删除第k个点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
【单调队列】
基本声明方式
struct Node
{
int v, idx;
};
deque<Node> dq;
for (int i = 1; i <= n; i++) // 遍历数组
{
while (!dq.empty() && dq.front().idx <= i - k)
dq.pop_front();
// 维护minq队列:保持队列单调递增
// 如果修改为dq.back().v <= a[i],则维护maxq队列:保持队列单调递减
while (!dq.empty() && dq.back().v >= a[i])
dq.pop_back();
dq.push_back({a[i], i}); // dq.front().v为最小值
// 按照题目要求输出最小值、最大值等
}
优化动态规划
// 用于优化动态规划
// 队列头存放dp[]的最小值,每次将 {dp[i], i} 插入队列
// 状态转移方程
dp[i] = dq.front().v + a[i];
// 超出窗口范围的元素,dq.front().idx < i - k,需要弹出队首
while (!dq.empty() && dq.front().idx < i - k)
dq.pop_front();
// 维护minq队列:保持队列单调递增
while (!dq.empty() && dq.back().v >= dp[i])
dq.pop_back();
dq.push_back({dp[i], i});
题解:AtCoder AT_awc0033_e Minimum Cost of Stepping Stones
【栈】
基本操作
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[++tt] = x;
// 从栈顶弹出一个数
tt--;
// 栈顶的值
stk[tt];
// 判断栈是否为空,如果 tt > 0,则表示不为空
if (tt > 0) not empty
else empty
【并查集】
基本操作
// (1)朴素并查集+路径压缩:
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
void merge(int a, int b)
{
p[find(a)] = find(b);
}
// (2)维护size的并查集:
int p[N], siz[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
void merge(int a, int b)
{
if (find(a)==find(b)) return;
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
}
// (3)维护到祖宗节点距离的并查集:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = d[y]-d[x]; // X和Y是同类
d[find(a)] = d[y]+1-d[x]; // X吃Y
【线段树】
区修+区查
int n, m;
int w[N];
struct Node
{
int l, r;
int dt, mn; // (根据题目要求变化)求最小值,dt是懒标记
}tr[N*4];
void pushup(int u) // 由子节点的信息,来计算父节点的信息
{
tr[u].mn = min(tr[u<<1].mn, tr[u<<1|1].mn); // (根据题目要求变化)求最小值
// 如果要求区间和
// tr[u].sum = tr[u<<1].sum + tr[u<<1|1].sum
}
void pushdown(int u)
{
auto &root = tr[u], &l = tr[u<<1], &r = tr[u<<1|1];
l.dt += root.dt, l.mn += root.dt;
r.dt += root.dt, r.mn += root.dt;
// 如果要求区间和
// l.sum += root.dt * (l.r - l.l + 1)
// r.sum += root.dt * (r.r - r.l + 1)
root.dt = 0;
}
void build(int u, int l, int r)
{
if (l==r) tr[u] = {l, r, 0, w[l]};
else
{
tr[u] = {l, r};
int mid = l+r >> 1;
build (u<<1, l, mid), build(u<<1|1, mid+1, r);
pushup(u);
}
}
void update(int u, int l, int r, int d)
{
if (tr[u].l>=l && tr[u].r<=r)
{
tr[u].dt += d, tr[u].mn += d; // (根据题目要求变化)修改值
// 如果要求区间和
// tr[u].sum += d * (tr[u].r - tr[u].l + 1)
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l<=mid) update(u<<1, l, r, d);
if (r>mid) update(u<<1|1, l, r, d);
pushup(u);
}
}
int query(int u, int l, int r)
{
if (tr[u].l>=l && tr[u].r<=r) // 树中节点,已经被完全包含在[l,r]中了
{
return tr[u].mn;
// 如果要求区间和
// return tr[u].sum;
}
else
{
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
int res = INF; // (根据题目要求变化)设最大值
if (l<=mid) res = query(u<<1, l, r);
if (r>mid) res = min(res, query(u<<1|1, l, r)); // 求最小值
// 如果要求区间和
// if (l<=mid) res += query(u<<1, l, r);
// if (r>mid) res += query(u<<1|1, l, r); // 求最小值
return res;
}
}
点修+区查+二分
int n, m;
int w[N];
struct Node
{
int l, r; // 区间左右端点
int mx; // mx: 区间最大值
}tr[N * 4]; // 线段树数组,开4倍空间
void pushup(int u)
{
tr[u].mx = max(tr[u << 1].mx, tr[u << 1 | 1].mx); // 取左右子树的最大值
}
void build(int u, int l, int r)
{
if (l == r) // 叶节点
{
tr[u] = {l, r, c[l]}; // 初始懒标记为0,值为机器容量
}
else
{
tr[u] = {l, r};
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 递归构建左右子树
pushup(u); // 更新当前节点的值
}
}
// 单点更新:将位置pos的值更新为d
void update(int u, int pos, int d)
{
if (tr[u].l == tr[u].r) // 找到叶节点
{
tr[u].mx = d; // 更新值
return;
}
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (pos <= mid) // 在左子树
{
update(u << 1, pos, d);
}
if (pos > mid) // 在右子树
{
update(u << 1 | 1, pos, d);
}
pushup(u); // 更新当前节点的值
}
}
// 区间查询:查询区间[l, r]的最大值
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r) // 当前节点完全包含在查询区间内
{
return tr[u].mx;
}
else
{
int mid = tr[u].l + tr[u].r >> 1;
int res = -INF; // 初始化为负无穷
if (l <= mid) // 与左子树有交集
{
res = query(u << 1, l, r);
}
if (r > mid) // 与右子树有交集
{
res = max(res, query(u << 1 | 1, l, r)); // 取最大值
}
return res;
}
}
// 在区间[l, r]中查找第一个大于等于val的位置,找不到返回-1
int find(int u, int l, int r, int val)
{
if (tr[u].mx < val) // 当前区间最大值小于val,不可能找到
{
return -1;
}
if (tr[u].l == tr[u].r) // 叶节点
{
return tr[u].l; // 返回位置
}
int mid = (tr[u].l + tr[u].r) >> 1;
int res = -1;
if (l <= mid) // 先在左子树找
{
res = find(u << 1, l, r, val);
}
if (res == -1 && r > mid) // 左子树没找到,在右子树找
{
res = find(u << 1 | 1, l, r, val);
}
return res;
}
题解:AtCoder AT_awc0020_e Shelving Books on a Bookshelf
【树状数组】
基本操作
int lowbit(int x) //提出x的低位2次幂数
{
return x & -x;
}
void add(int x, int c) //向后修
{
for (int i=x; i<=n; i+=lowbit(i)) tr[i] += c;
}
int query(int x) //向前查
{
int res = 0;
for (int i=x; i; i-=lowbit(i)) res += tr[i];
return res;
}
// 初始化树状数组
for (int i = 1; i <= n; i++)
cin >> a[i];
// 初始化树状数组
// 将原始数组转换为差分数组存储到树状数组中
for (int i = 1; i <= n; i++)
add(i, a[i] - a[i - 1]); // 存储差分值
阈值分治解决技巧
// 阈值分治解决方案
if (k <= B) // 小k,使用分块标记
tag[k] += v;
else // 大k,直接修改树状数组
{
for (int i = k; i <= n; i += k)
add(i, v);
}
int x;
cin >> x; // 查询前缀和
int ans = sa[x] + query(x); // 基础前缀和+树状数组修改
for (int k = 1; k <= B; k++) // 加上分块标记的贡献
ans += tag[k] * (x / k);
cout << ans << endl;
题解:AtCoder AT_awc0032_e Multiple Bonus
区间不同物种的数量查询
// 区间查询物品数量,可以使用last[]保存物品最后一次出现位置,按照 r 从小到大排序后一边查询,一边修改
int cur = 1; // 当前处理的数组位置
for (int i = 1; i <= q; i++) // 处理每个查询
{
int l = Q[i].l, r = Q[i].r, id = Q[i].id;
// 处理数组位置,直到达到当前查询的右端点
while (cur <= r)
{
int species = p[cur]; // 当前位置的物种
if (last[species] != 0) // 如果这个物种之前出现过
{
add(last[species], -1); // 在之前出现的位置-1
}
add(cur, 1); // 在当前位置+1
last[species] = cur; // 更新这个物种最后出现的位置
cur++;
}
// 查询区间[l, r]的不同物种数量
ans[id] = query(r) - query(l - 1);
}
浙公网安备 33010602011771号