[总结] 基础算法(一)

内容

  • STL
  • 二分
  • \(0/1\) 分数规划

STL

都有一定的常数,谨慎使用

#include<algorithm>

区间基本上都是左开右闭的。

  • \(sort\) 的本质是重载小于号。 \(sort(a,a+n,cmp)\)

nth_element

  • 作用是,调用完上述函数后,没有完整进行排序,但是 \(a[x]\) 之前的元素都比 \(a[x]\) 小,\(a[x]\) 后面的元素都比 \(a[x]\) 大。也就是求出了第x 小元素,\(a[x]\)。(单次复杂度\(O(n)\) )

  • \(nth\_element(a+1,a+1+n,a+x,cmp)\)


unique 与离散化

  • 把一个(有序)数组去重,返回不重复的结尾的迭代器的下一位置。
  • unique 也可以自定义cmp 函数,\(unique(a+1,a+n+1,cmp)\) 这样调用。cmp(A,B): 需要在A==B 时返回 true,其他情况 false。(比较少用)
  • 只能把相邻的相同的元素进行去重,因此需要是有序的。
  • 如果返回不重复的结尾,可以再调用后减一(或减去 \(a+1\)

离散化

  • 进行映射(只考虑大小关系的情况下),进行数值压缩
  • 需要辅助空间的空间开销
//对a数组进行离散化 
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 1e5+10;
int a[maxn],n;
int c[maxn],tot;//辅助空间
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d",a+i),c[i]=a[i];
	sort(c+1,c+1+n);
	tot=unique(c+1,c+1+n)-(c+1);
	for(int i=1;i<=n;i++)a[i]=lower_bound(c+1,c+1+tot,a[i])-c;
	for(int i=1;i<=n;i++)printf("%d ",a[i]);
	return 0;
}

vector

  • 下标从 \(0\) 开始,到 \(size()-1\) 结束
#include <vector>
vector<int> A;
vector<int> B(A);//A复制一份
vector<int> B(n);//开n个0
vector<int> B(n,i);//开n个i
  • \(A.front()\)\(A.back()\) 分别指的是开头和结尾的元素

  • \(A.insert(it,x,n)\) ,在 it 迭代器后插入一(n)个元素,复杂度和数组暴力插入一样。

  • \(A.erase(it)\); \(A.erase(it1,it2)\):擦除 \(it1\)\(it2\) 之间的这些元素。

测试代码:

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> A;
vector<int> :: iterator it;
int main(){
	for(int i=10;i>=1;i--)A.push_back(i*10);
	sort(A.begin(),A.end());
	for(it=A.begin();it!=A.end();it++){
		printf("%d ",*it);
	}
	puts("");
	for(int i=0;i<10;i++)printf("%d ",A[i]);
	puts("");
	printf("size: %d\nempty: %d\nfront: %d\nback: %d",A.size(),A.empty(),A.front(),A.back());
}

set

  • 本质是一个平衡树,常用来维护前驱后继
  • \(s.empty()\),判断是否为空。
  • \(s.size()\),返回 s 的大小。
  • \(s.insert(x)\),插入元素 x 。
  • \(s.erase(x) \ or \ s.erase(it)\), 把值为 x 的元素或 it 迭代器所指元素删除
  • \(s.clear()\) ,清空集合
  • \(it=s.find(x)\),返回 \(x\) 元素的迭代器,如果不存在,返回 end( )

set 的二分

  • \(s.lower\_bound(x)\)
  • \(s.upper\_bound(x)\)

set 结构体的代码实现(重载小于号)

#include <iostream>
#include <set>
#include <cstdio>
using namespace std;
struct node{
	int x,y;
	bool operator <(node a){
		if(x!=a.x)return x<a.x;
		return y<a.y;
	}
};
bool operator <(node a,node b){
		if(a.x!=b.x)return a.x<b.x;
		return a.y<b.y;
}
set<node> s;
int main(){
	for(int i=1;i<=10;i++){
		node t;
		t.x=i/2;t.y=i;
		s.insert(t);
	}
	for(set<node>::iterator it=s.begin();it!=s.end();it++)cerr<<it->x<<" "<<it->y<<endl;
	printf("%d\n",(int)s.size());
}

multiset

  • 操作类似于 set ,可以用来维护可重复的前驱后继
  • \(multiset\ erase\) 一个元素时会把相等的元素都删掉。

删除一个相等的元素:

it=A.find();
A.erase(it);//通过删除迭代器来实现

queue

  • \(q.front()\) 返回队首元素
  • \(q.back()\) 返回队尾元素
  • \(q.clear()\),清空,\(O(n)\)
  • \(iterator\) 操作

priority_queue

  • 是一个二叉堆
  • 通过加 \(greater\) 来编程小过程
  • 结构体需要定义 \(operator \ <\),定义过程比较特殊:他会认为大的更小,小的更大

deque

  • 双端队列
  • 可以 用\([\ ]\) 访问
  • 需要 \(include<deque>\)
  • \(vector\) 多了 \(push\_front()\)\(pop\_front()\)

pair

  • \(make\_pair\) 来得到一个 \(pair\)
  • \(x.first\)\(x.second\)

map

  • 红黑树,是平衡树的一种

  • 相当于一个 \(set<type(a),type(b)>\),重载了 \([\ \ ]\) 运算符

  • 如果不存在 \(first=a\) 的元素,便会新建一个。否则会修改这个元素


常见的 STL 用法

  1. \(sort\)
  2. \(set\) 去重
  3. \(bfs\) 运用 \(queue\)
  4. \(sort + unique + lower\_bound\) 进行离散化
  5. \(vector\) 边长数组
  6. \(map\) 作劣质哈希 (复杂度多一个 \(log\)
  7. \(priority\_queue\) 跑最短路

二分

  • 把求最优性问题转化为可行性问题

  • 通常指二分答案,是一种难题里的工具

  • 二分的题目答案必须有单调性

例题

P1182 数列分段 Section II

枚举和的最大值(答案),看看能不能符合题意。

#include <iostream>
#include <cstdio> 
using namespace std;
int n,m,a[100005],l,r,mid,ans;
inline bool check(int x)
{
    int tot=0,num=0;
    for(int i=1;i<=n;i++)
    {
        if(tot+a[i]<=x)tot+=a[i];
        else tot=a[i],num++;
    }
    num++;
    //printf("x:%d num:%d\n",x,num);
    return num>m;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%d",&a[i]),l=max(l,a[i]),r+=a[i];
    while(l<r)
    {
        mid=l+r>>1;
        if(check(mid))l=mid+1;
        else r=mid;
    }
    cout<<mid;
    return 0;
}

细节

  • 注意二分写法与 check 函数的对应关系

P1083 [NOIP2012 提高组] 借教室

差分+二分

  • 差分用来处理区间修改,利用其与前缀和的相反性质推得每一天的 \(need\)
  • 二分来二分答案(因为具有单调性)
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define LL long long
const int maxn = 1e6 + 6;
LL rest[maxn],dff[maxn],l[maxn],r[maxn],need[maxn],d[maxn];
int n,m;
void clear(){memset(dff,0,sizeof dff);return ;}
bool check(int x){
	clear();
	for(int i=1;i<=x;i++){
		dff[l[i]]+=d[i];
		dff[r[i]+1]-=d[i];
	}
	for(int i=1;i<=n;i++){
		need[i]=need[i-1]+dff[i];
		if(need[i]>rest[i])return false;
	}
	return true;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",rest+i);
	for(int i=1;i<=m;i++)scanf("%lld%lld%lld",d+i,l+i,r+i);
	if(check(m)){
		puts("0");return 0;
	}
	int L=1,R=m,mid;
	while(L<R){
		mid=L+R>>1;
		if(check(mid))L=mid+1;
		else R=mid;
	}
	printf("-1\n%d",L);
	return 0;
}

后记

  • 可以通过差分来修改原序列并得到原序列

CF1360H Binary Median

通过确定删除 \(n\) 个数后中位数的位置,来求得中位数


二分适用

  • 看上去很难直接找到最优解
  • 题目答案具有单调性
  • 可以判断二分出来的 \(mid\) 的合法性

二分的本质是加速枚举答案,每次把答案区间除以二


0/1 分数规划

  • 每一个元素有两个属性 \(a_i \ b_i\)
  • 选择一些元素使得 \(\sum a_i/\sum b_i\) 达到最大或最小
  • \(e.g.\) 最优比例生成树

做法

  • 通过二分实现,二分一个 \(mid\)

\[\frac{\sum ai}{\sum bi}\geq mid\\ \ \ \ => \sum ai-mid*\sum bi>=0\\ => \sum(ai-mid*bi)>=0 \]

回头看最优比例生成树,我们就可以给每一条边赋值求一棵最大生成树


例题

P4377 [USACO18OPEN]Talent Show G

使得 \(\sum(ti-mid*wi)\geq0\) 并且 \(\sum\ wi\geq W\)

\(0/1\) 分数规划和背包问题(\(wi\)虽然很大但是 \(W\) 小)

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 260 , maxw = 1000 + 10;
const int INF = 0x3f3f3f3f;
const double eps=1e-5;
int n,W;
int w[maxn],t[maxn];
double c[maxn],f[maxw];
bool check(double x){
	for(int i=1;i<=n;i++)c[i]=(double)(t[i]-x*w[i]);//c作为转移的辅助空间
	for(int i=1;i<=W;i++)f[i]=-INF;
	for(int i=1;i<=n;i++){//0/1背包 
		for(int j=W+w[i];j>=w[i];j--){ 
			if(j>=W)f[W]=max(f[W],f[j-w[i]]+c[i]);
			else f[j]=max(f[j],f[j-w[i]]+c[i]);
		}
	}
	return f[W]>=0;
}
int main(){
	scanf("%d%d",&n,&W);
	double l=0.0,r=0.0;
	for(int i=1;i<=n;i++)scanf("%d%d",w+i,t+i),r+=t[i];
	while(l+eps<=r){
		double mid=(l+r)/2;
		if(check(mid))l=mid;
		else r=mid;
	}
	printf("%d",(int)(l*1000));
	return 0;
}

后记

  • 一般 \(0/1\) 分数规划会结合背包来考察
  • 需要用到实数二分和 \(double\)