Adongua的算法模板
⭐如何根据数据范围判断大致时间复杂度
数据范围 | 时间复杂度 | 相应算法 |
---|---|---|
n<=30 | 指数级别 | 暴搜、dfs+剪枝、状压dp |
n<=100 | O(n3) | floyd、dp |
n<=1000 | O(n2)、O(n2logn) | dp,二分,朴素dij、朴素Prim、Bellman-Ford |
n<=10000 | O(n* n \sqrt{n} n) | 块状链表、分块、莫队 |
n<=100000 | O(nlogn) | 各种sort,线段树、树状数组、set/map、heap、拓扑排序、堆优化dij、堆优化prim、spfa、求凸包、求半平面交、二分、线性dp |
n<=1000000 | O(n) , 以及常数较小的 O(nlogn) 算法 | hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O(nlogn)的做法:sort、树状数组、heap、堆优化dij、spfa、线性dp |
n<=10000000 | O(n) | 双指针扫描、kmp、AC自动机、线性筛素数 |
n<=1e9 | O( n \sqrt{n} n) | 判断质数 |
n<=1e18 | O(logn) | 最大公约数,快速幂 |
n<=1e1000 | O((logn)2) | 高精度加减乘除 |
n<=1e100000 | O(logn×loglogn) | 高精度加减、FFT/NTT |
一、STL
1.set
①单元素应用
set<int> s; //以int型为例 默认按键值升序
set<int,greater<int>> s; //降序排列
int x;
s.insert(x); //将x插入q中
s.erase(x); //删除q中的x元素,返回0或1,0表示set中不存在x
s.clear(); //清空q
s.empty(); //判断q是否为空,若是返回1,否则返回0
s.size(); //返回q中元素的个数
s.find(x); //在q中查找x,返回x的迭代器,若x不存在,则返回指向q尾部的迭代器即 q.end()
s.lower_bound(x); //返回一个迭代器,指向第一个键值不小于x的元素(二分)
s.upper_bound(x); //返回一个迭代器,指向第一个键值大于x的元素(二分)
s.rend(); //返回第一个元素的的前一个元素迭代器
s.begin(); //返回指向q中第一个元素的迭代器
s.end(); //返回指向q最后一个元素下一个位置的迭代器
s.rbegin(); //返回最后一个元素
②多元素应用
struct Node{
int a,b;
bool operator< (const Node node)const
{
return a>node.a; //按a的值升序
}
};
set<Node> s;
2.priority_queue
①基本操作
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列,比如可以原地构造一个二元组塞进去,例:pq.emplace(2,4)
pop 弹出队头元素
swap 交换内容
②改为最小堆
//一般情况
priority_queue<int,vector<int>,greater<int> pq;
//pair二元组情况
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
//结构体情况
struct node{
int x;
int y;
node(){}
node(int _x,int _y):x(_x),y(_y){}
};
//大顶堆
struct cmp1{
bool operator ()(node a,node b){
if(a.x==b.x){
return a.y<b.y;
}
return a.x<b.x;
}
};
//小顶堆
struct cmp2{
bool operator ()(node a,node b){
if(a.x==b.x){
return a.y>b.y;
}
return a.x>b.x;
}
};
priority_queue<node,vector<node>,cmp1> pq1;
priority_queue<node,vector<node>,cmp2> pq2;
3.STL建堆
1)建立堆
make_heap(_First, _Last, _Comp)
默认是建立最大堆的。对int类型,可以在第三个参数传入greater< int > ( )得到最小堆。
2)在堆中添加数据
push_heap (_First, _Last,Comp)
要先在容器中加入数据,再调用push_heap ()
3)在堆中删除数据
pop_heap(_First, _Last,Comp)
要先调用pop_heap()再在容器中删除数据,即调用vector的pop_back()删除尾部元素,当然它实际上只是把size变了一下罢了
4)堆排序
sort_heap(_First, _Last,Comp)
排序之后就不再是一个合法的heap了
4.vector
//vec定长数组的定义
//一维
vector<type> vec(n,0);
//二维
vector<vector<type>> vec(m,vector<type> (n,0));//表示定义一个m行n列的vector二维数组,并将元素都初始化为0
//vec的insert与erase
vec.insert(iter,x):在迭代器iter的位置插入x,原iter位置及之后的元素后移
vec.erase(iter):删除迭代器iter位置的元素
5.二分查找:upper_bound与lower_bound
①概述
upper_bound与lower_bound
(注意:使用之前需确保容器元素整体有序,若是递减需在()最后一个参数位置加greater<int>())
lower_bound(a,a+n,val,cmp)/lower_bound(a.begin(),a.end(),val,cmp):返回数组 a 中大于等于 val 的第一个元素的地址,若 a 中的元素均小于 val 则返回尾后地址。
upper_bound(a,a+n,val,cmp)/upper_bound(a.begin(),a.end(),val,cmp):函数返回数组 a 中大于 val 的第一个元素的地址,若 a 中的元素均小于等于 val 则返回尾后地址
②示例
//a为递增序列
#define ins(x) (a.insert(upper_bound(a.begin(),a.end(),x),x)) // 插入x:在a数组中用二分找到第一个大于x的元素的位置插入x,这样x就会被放在那个元素的前面,一直这么做便可保证单调不减
#define que(x) (a[x - 1]) // 查询排名为x的数
#define del(x) (a.erase(lower_bound(a.begin(),a.end(),x))) // 删除值为x的数:在a数组中用二分找到第一个大于等于x的元素,将其彻底删除,其实就是在a中找到等于x的元素删除,毕竟之前放进去过
6.pbds平衡树
①基础操作
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#define PII pair<int, int>
#define mp_(x, y) make_pair(x, y)
tree<int, null_type, less<int>, rb_tree_tag, tree_order_statistics_node_update> tr;
/// rb_tree_tag 和 splay_tree_tag 选择树的类型(红黑树和伸展树)
null_type//无映射(g++为null_mapped_type)
less<PII>//从小到大排序
tree_order_statistics_node_update//更新方式
tr.insert(mp_(x, y));//插入
tr.erase(mp_(x, y));//删除
tr.order_of_key(PII(x, y));//求排名
tr.find_by_order(x);//找k小值,返回迭代器
tr.join(b);//将b并入tr,前提是两棵树类型一致并且二没有重复元素
tr.split(v, b);//分裂,key小于等于v的元素属于tr,其余属于b
tr.lower_bound(x);//返回第一个大于等于x的元素的迭代器
tr.upper_bound(x);//返回第一个大于x的元素的迭代器
//以上的所有操作的时间复杂度均为O(logn)
//注意,插入的元素会去重,如set
//显然迭代器可以++,--运算
②rb树
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#define Node pair<int,int>
#define debug(x) cout << x << "ok" << endl
#define file freopen("1.in", "r", stdin);freopen("1.out", "w", stdout);
using namespace std;
using namespace __gnu_cxx;
using namespace __gnu_pbds;
typedef long long ll;
const int N = 2e5 + 7, M = 1e5 + 7, INF = 0x3f3f3f3f;
tree<ll, null_type, less<ll>, rb_tree_tag, tree_order_statistics_node_update> tr;
int n, m;
ll k, ans;
ll read()
{
ll x = 0, f = 1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}
while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}
return x * f;
}
int main()
{
n = read();
for(int i = 1; i <= n; ++ i){
int op = read();
k = read();
if(op == 1)tr.insert((k << 20) + i);
if(op == 2)tr.erase(tr.lower_bound(k << 20));
if(op == 3)printf("%d\n", tr.order_of_key(k << 20) + 1);
if(op == 4)ans = *tr.find_by_order(k - 1), printf("%lld\n", ans >> 20);
if(op == 5)ans = *-- tr.lower_bound(k << 20), printf("%lld\n", ans >> 20);
if(op == 6)ans = *tr.upper_bound((k << 20) + n), printf("%lld\n", ans >> 20);
}
return 0;
}
③splay树
#include<bits/stdc++.h>
#include<ext/pb_ds/assoc_container.hpp>
#include<ext/pb_ds/tree_policy.hpp>
#define Node pair<int,int>
using namespace std;
using namespace __gnu_pbds;
map <int,int> s;
tree< Node ,null_type,less< Node >,splay_tree_tag,tree_order_statistics_node_update> T;
int n,op,x;
int main()
{
scanf("%d",&n);
for(register int i = 1; i <= n; i++)
switch(scanf("%d%d",&op,&x), op)
{
case 1 :T.insert(Node(x,s[x]++));
break;
case 2 :T.erase(Node(x,--s[x]));
break;
case 3 :printf("%d\n",(int)T.order_of_key(Node(x,0))+1);
break;
case 4 :printf("%d\n",T.find_by_order(x-1)->first);
break;
case 5 :printf("%d\n",T.find_by_order(
T.order_of_key(Node(x,0))-1
)->first);
break;
case 6 :printf("%d\n",T.find_by_order(
T.order_of_key(Node(x,s[x]-1))+(T.find(Node(x,0)) == T.end() ? 0 : 1)
)->first);
break;
default:break;
}
return 0;
}
7.unique函数
①概述
unique函数可以删除有序数组中的重复元素。
注意:
(1) 这里的删除不是真的delete,而是将重复的元素放到容器末尾
(2) unique函数的返回值是去重之后的尾地址
(3) 一定要先对数组进行排序才可以使用unique函数
②示例
//vec版本
sort(vec.begin(),vec.end());
auto it1=vec.begin(),it2=vec.end();
auto it=unique(it1,it2);
vec.erase(it,it2);
print(vec)
//数组版本
sort(arr,arr+n);
int len;//用一个len来记录无重复元素的数组的长度
len=unique(arr,arr+n)-arr;
print(arr,0,len);
8.stringstream解决一行不知道输入多少个数的输入问题
type a[maxn];
int cnt=0;
string line;
getline(cin,line); // 先读入整一行字符串
stringstream ssin(line); // 用字符串初始化流输入
while(ssin >> a[cnt]) cnt ++; // 流输入
9.判断字母大小写及数字
isupper(ch);//判断是否为大写字母
islower(ch);//判断是否为小写字母
isdigit(ch);//判断是否为数字
10.正取模
int mod(int x,int p)
{
return (x+p)%p;
}
二、数据结构
1.树状数组
①基本操作
(1)lowbit操作
lowbit(x)=x&-x
定义:表示非负整数x在二进制下最低位1及其后面的0构成的数值,例如4=100,则lowbit(4)=4
(1)单点修改
对原数组a的某个下标对应的元素a[i]做+d操作,则对于树状数组那个下标对应的t[i]及其祖先节点的元素都要做+d操作,根据图像可知t[i]的祖先节点元素的数组下标就是每次i+lowbit(i),则有板子如下:
//单点修改
void add(int x,int d)
{
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=d;
}
(2)查询前缀和
对于原数组求到i的前缀和,则就a从1开始一直加到i,根据图像易知等价于对树状数组往左上方向的每一个t的元素求和(例如根据图像我们可求到下标为5的前缀和为t[4]+t[5]=(a[1]+a[2]+a[3]+a[4])+a[5]),下标变化为i-lowbit(i),则有板子如下:
//查询前缀和
int ask(int x)
{
int ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=t[i];
return ans;
}
②操作总结
(1)单点修改、单点查询
//维护原数组a的前缀和
add(x,k);
ask(x)-ask(x-1)
(2)单点修改、区间查询
//维护原数组a的前缀和
add(x,k);
ask(r)-ask(l-1)
(3)区间修改、单点查询
//维护原数组a的差分数组b的前缀和
for(i) add(i,b)
add(l,d),add(r+1,-d);
ask(x)
//示例:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+1;
int n,m;
int a[maxn],t[maxn];
int lowbit(int x)
{
return x&-x;
}
void add(int x,int d)
{
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=d;
}
ll ask(int x)
{
ll ans=0;
for(int i=x;i;i-=lowbit(i))
{
ans+=t[i];
}
return ans;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);//
for(int i=0;i<m;i++)
{
char op[2];
scanf("%s",op);
if(op[0]=='Q')
{
int x;
scanf("%d",&x);
printf("%lld\n",ask(x));
}
else
{
int l,r,d;
scanf("%d%d%d",&l,&r,&d);
add(l,d),add(r+1,-d);
}
}
}
③复杂操作
(1)区间修改、区间查询
在区间修改,单点查询基础上,想着再用一个树状数组去维护i*b[i]的前缀和,则
区间修改就有:add(t1,l,d),add(t1,r+1,-d),add(t2,l,i*d),add(t2,r+1,(r+1)*(-d));
区间查询就有:(r+1)*ask(t1,r)-ask(t2,r)-(l*ask(t1,l-1)-ask(t2,l-1))。
//示例:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+1;
int n,m;
int a[maxn];
ll t1[maxn],t2[maxn];
int lowbit(int x)
{
return x&-x;
}
void add(ll t[],int x,ll d)
{
for(int i=x;i<=n;i+=lowbit(i)) t[i]+=d;
}
ll ask(ll t[],int x)
{
ll ans=0;
for(int i=x;i;i-=lowbit(i)) ans+=t[i];
return ans;
}
ll getSum(int x)//区间查询
{
return (x+1)*ask(t1,x)-ask(t2,x);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
int b=a[i]-a[i-1];
add(t1,i,b);
add(t2,i,(ll)i*b);
}
for(int i=0;i<m;i++)
{
char op[2];
scanf("%s",op);
int l,r;
scanf("%d%d",&l,&r);
if(op[0]=='Q')
{
printf("%lld\n",getSum(r)-getSum(l-1));
}
else
{
int d;
scanf("%d",&d);
add(t1,l,d),add(t1,r+1,-d);
add(t2,l,l*d),add(t2,r+1,(r+1)*(-d));
}
}
}
2.线段树
①不带懒标记
//示例1:求区间最大连续子段和+单点修改
struct Tnode
{
int l,r;//当前节点表示[l,r]区间
int sum;//维护[l,r]的区间和
int lmax,rmax,tmax;//维护[l,r]区间的最大前缀子段和,最大后缀子段和,最大子段和
}tr[maxn*4];
void pushup(Tnode &u,Tnode &l,Tnode &r)
{
//u节点维护的区间和显然等于其左右节点维护的区间和的和
u.sum=l.sum+r.sum;
//u节点维护的区间最大前缀子段和等于左节点的区间最大前缀子段和与左节点的区间和+右节点的最大前缀子段和中最大的那个
u.lmax=max(l.lmax,l.sum+r.lmax);//tip:显然,对于u维护的区间,最大前缀子段和要么就是单纯往左边看,即左区间的那个最大前缀子段和,要么就是加上了右区间之后,可能值变大,因此再多考虑个左区间的和加上右区间的最大前缀子段和
//u节点维护的区间最大后缀子段和等于右节点的区间最大后缀子段和与右节点的区间和+左节点的最大前缀子段和中最大的那个
u.rmax=max(r.rmax,r.sum+l.rmax);
//u节点维护的区间最大子段和等于左节点的区间最大子段和、右节点的区间最大子段和与左节点的最大后缀子段和+右节点的最大前缀子段和中最大的那个
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);
}
void pushup(int u)
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);//当前节点u信息更新
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],w[r],w[r],w[r]};//叶子节点
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 modify(int u,int x,int v)//单点修改
{
if(tr[u].l==x&&tr[u].r==x) tr[u]={x,x,v,v,v,v};//当前节点区间为[x,x],即已经为表示单点x的叶节点了,做修改
else
{
int mid=tr[u].l+tr[u].r>>1;//二分节点表示的区间,看要修改的点x是在左边还是右边
if(x<=mid) modify(u<<1,x,v);//否则递归修改左节点
else modify(u<<1|1,x,v);//否则递归修改右节点
pushup(u);//当前节点信息更新
}
}
Tnode query(int u,int l,int r)//区间查询,这里返回Tnode,因为需要用到Tnode的很多信息,所以不能只返回一个答案
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u];//当前节点维护的区间包含在了要查询的区间范围内,则直接return当前Tnode
else//否则
{
int mid=tr[u].l+tr[u].r>>1;
if(r<=mid) return query(u<<1,l,r);//如果要查询的区间靠左半边,则去查左节点(为什么这里不是l<=mid?因为这里不像上道题目求最大数一样是单个数,这里是求子段和,所以要求整个待查区间在节点区间左半边才能跳左节点去查,条件更为苛刻)
else if(l>mid) return query(u<<1|1,l,r);//如果要查询的区间靠右半边,则去查右节点
else//否则区间完全包含了节点维护的区间
{
auto left=query(u<<1,l,r);//则既要找左节点
auto right=query(u<<1|1,l,r);//又要找右节点
Tnode res;//申请一个结构体Tnode变量
pushup(res,left,right);//通过做当前节点u信息更新,在left和right中找到答案放到res中
return res;
}
}
}
int main()
{
build(1,1,n);//建树
op();
}
②带懒标记
//示例:区间修改+区间查询
struct Tnode
{
int l,r;
ll sum,add;//维护的区间的区间和,加法修改的懒标记
}tr[maxn*4];
void pushup(int u)//上推操作
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//当前节点维护的区间的区间和,显然等于子节点维护的区间到的区间和的和
}
void pushdown(int u)//下推操作
{
Tnode &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];//根、左儿子、右儿子
if(root.add)//如果懒标记不为0
{
left.add+=root.add,left.sum+=(ll)(left.r-left.l+1)*root.add;//左儿子懒标记、区间和更新
right.add+=root.add,right.sum+=(ll)(right.r-right.l+1)*root.add;//右儿子懒标记、区间和更新
root.add=0;//根懒标记复原
}
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],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);//用子节点信息更新当前节点
}
}
void modify(int u,int l,int r,int d)
{
if(tr[u].l>=l&&tr[u].r<=r)//当前节点维护的区间包含在了要修改的区间内
{
tr[u].sum+=(ll)(tr[u].r-tr[u].l+1)*d;//当前节点的区间和显然要加上区间长度*d
tr[u].add+=d;//当前节点的懒标记加d
}
else//否则
{
pushdown(u);//用当前节点的信息更新子节点信息
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid) modify(u<<1,l,r,d);//要修改的区间在当前节点维护的区间的左半边
if(r>mid) modify(u<<1|1,l,r,d);//要修改的区间在当前节点维护的区间的右半边
pushup(u);//用子节点的信息更新当前节点的信息
}
}
ll query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;//当前节点维护的区间包含在了询问的区间内
pushdown(u);//用当前节点的信息更新子节点的信息(要进入子节点前必须做的操作,不然信息更新会有误)
int mid=tr[u].l+tr[u].r>>1;
ll sum=0;
if(l<=mid) sum+=query(u<<1,l,r);//询问的区间在当前节点维护的区间的左半边
if(r>mid) sum+=query(u<<1|1,l,r);//询问的区间在当前维护的区间的右半边
return sum;
}
int main()
{
build(1,1,n);//建树
op();
}
3.AC自动机
1.Kmp算法
①ne数组
则我们套用前缀函数的计算方式可以直接写出求ne数组的如下代码:
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度(scanf("%s%s",s+1,p+1))
//求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )//j从0开始
{
while (j && p[i] != p[j + 1]) j = ne[j];//当j还在范围内并且失配就回跳
if (p[i] == p[j + 1]) j ++ ;//如果匹配至多加1
ne[i] = j;//j赋值ne[i]
}
②匹配算法
最后匹配算法如下:
// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == m)
{
j = ne[j];
// 匹配成功后的逻辑
}
}
2.Trie树
①数据结构
int son[maxn][26], cnt[maxn], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// maxn为总的节点数,取决于输入字符串个数与每个字符串长度
②insert函数
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}
③query函数
// 查询字符串出现的次数
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
3.AC自动机与Trie图
用O(n) 时间求出每一个单词 出现在哪些地方及出现次数
/*
//AC自动机
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e4+1,maxs=55,maxm=1e6+1;
int n;
int tr[maxn*maxs][26],cnt[maxn*maxs],idx;//tr[i][j]表示i的子节点,且子节点表示的字符为'a'+j;cnt[i]表示i节点位置时的单词数目;idx表示节点的个数
char str[maxm];
int ne[maxn*maxs];//对应kmp的next数组,next[j]表示以j节点结尾的后缀串能够匹配到的最长的非平凡前缀串的尾字符的节点
void insert()//插入函数
{
int p=0;
for(int i=0;str[i];i++)
{
int t=str[i]-'a';//字符转化成下标
if(!tr[p][t]) tr[p][t]=++idx;//如果'a'+t字符此前没有作为当前节点的子节点而存在,则计入子节点
p=tr[p][t];//深入子节点
}
cnt[p]++;//每插入一个单词,单词结尾的字符对应的节点的cnt就++,表示以该字符结尾的单词数多1
}
void build()//bfs得到ne数组
{
queue<int> q;
for(int i=0;i<26;i++)//先将所有单个字符入队
{
if(tr[0][i])
{
q.push(tr[0][i]);
}
}
while(!q.empty())
{
int t=q.front();
q.pop();
for(int k=0;k<26;k++)
{
int i=tr[t][k];//得到t节点的子节点,对应kmp计算next数组中的i
if(!i) continue;
int j=ne[t];//将跳的位置初始化为ne[t]
while(j&&!tr[j][k])//当j没有出界并且j没有'a'+k这个子节点字符,对应kmp计算next数组中的p[i]!=p[j+1]
{
j=ne[j];//回跳
}
if(tr[j][k]) j=tr[j][k];//如果j有'a'+k这个子节点字符,则j就等于tr[j][k]
ne[i]=j;//把i的ne赋值成j
q.push(i);//i入队
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(tr,0,sizeof tr);
memset(cnt,0,sizeof cnt);
memset(ne,0,sizeof ne);
idx=0;
scanf("%d",&n);
for(int i=0;i<n;i++)//建立trie树
{
scanf("%s",str);
insert();//向trie树插入节点
}
build();//得到ne数组
scanf("%s",str);
int res=0;
for(int i=0,j=0;str[i];i++)//类似kmp
{
int t=str[i]-'a';
while(j&&!tr[j][t]) j=ne[j];//j的子节点不存在'a'+t这个字符,对应kmp的s[i]!=p[j+1]
if(tr[j][t]) j=tr[j][t];//对应kmp的s[i]==p[j+1]
int p=j;
while(p)//p遍历所有p回跳指向的节点,直到不存在回跳指向了
{
res+=cnt[p];//结果数+当前节点结尾的单词数
cnt[p]=0;
p=ne[p];
}
}
printf("%d\n",res);
}
}
*/
//trie图
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e4+1,maxs=55,maxm=1e6+1;
int n;
int tr[maxn*maxs][26],cnt[maxn*maxs],idx;//tr[i][j]表示i的子节点,且子节点表示的字符为'a'+j;cnt[i]表示i节点位置时的单词数目;idx表示节点的个数
char str[maxm];
int ne[maxn*maxs];//对应kmp的next数组,next[j]表示以j节点结尾的后缀串能够匹配到的最长的非平凡前缀串的尾字符的节点
void insert()//插入函数
{
int p=0;
for(int i=0;str[i];i++)
{
int t=str[i]-'a';//字符转化成下标
if(!tr[p][t]) tr[p][t]=++idx;//如果'a'+t字符此前没有作为当前节点的子节点而存在,则计入子节点
p=tr[p][t];//深入子节点
}
cnt[p]++;//每插入一个单词,单词结尾的字符对应的节点的cnt就++,表示以该字符结尾的单词数多1
}
void build()//bfs得到ne数组
{
queue<int> q;
for(int i=0;i<26;i++)//先将所有单个字符入队
{
if(tr[0][i])
{
q.push(tr[0][i]);
}
}
while(!q.empty())
{
int t=q.front();
q.pop();
for(int k=0;k<26;k++)
{
int i=tr[t][k];//得到t节点的子节点,对应kmp计算next数组中的i
if(!i) tr[t][i]=tr[ne[t]][i];//如果t节点不存在'a'+i这个字符作为子节点,则将t回跳后的节点的那个字符的节点赋值给它
else
{
ne[i]=tr[ne[t]][i];//将t回跳后的节点的那个字符的节点赋值给ne[i]
q.push(i);
}
}
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
memset(tr,0,sizeof tr);
memset(cnt,0,sizeof cnt);
memset(ne,0,sizeof ne);
idx=0;
scanf("%d",&n);
for(int i=0;i<n;i++)//建立trie树
{
scanf("%s",str);
insert();//向trie树插入节点
}
build();//得到ne数组
scanf("%s",str);
int res=0;
for(int i=0,j=0;str[i];i++)//类似kmp
{
int t=str[i]-'a';
j=tr[j][t];//j直接回跳
int p=j;
while(p)//p遍历所有p回跳指向的节点,直到不存在回跳指向了
{
res+=cnt[p];//结果数+当前节点结尾的单词数
cnt[p]=0;
p=ne[p];
}
}
printf("%d\n",res);
}
}
4.平衡树
1.Treap
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
const int Inf=0x3f3f3f3f;
struct Node
{
int l,r;
int key,val;
int cnt,size;
}tr[maxn];
int n;
int root,idx;
void pushup(int p)
{
tr[p].size=tr[tr[p].l].size+tr[tr[p].r].size+tr[p].cnt;
}
int getNode(int key)
{
tr[++idx].key=key;
tr[idx].val=rand();
tr[idx].cnt=tr[idx].size=1;
return idx;
}
void zig(int &p)
{
int q=tr[p].l;
tr[p].l=tr[q].r,tr[q].r=p,p=q;
pushup(tr[p].r),pushup(p);
}
void zag(int &p)
{
int q=tr[p].r;
tr[p].r=tr[q].l,tr[q].l=p,p=q;
pushup(tr[p].l),pushup(p);
}
void build()
{
getNode(-Inf),getNode(Inf);
root=1,tr[1].r=2;
pushup(root);
if(tr[1].val<tr[2].val) zag(root);
}
void insert(int &p,int key)
{
if(!p) p=getNode(key);
else if(tr[p].key==key) tr[p].cnt++;
else if(tr[p].key>key)
{
insert(tr[p].l,key);
if(tr[tr[p].l].val>tr[tr[p].r].val) zig(p);
}
else
{
insert(tr[p].r,key);
if(tr[tr[p].r].val>tr[tr[p].l].val) zag(p);
}
pushup(p);
}
void remove(int &p,int key)
{
if(!p) return;
if(tr[p].key==key)
{
if(tr[p].cnt>1) tr[p].cnt--;
else if(tr[p].l||tr[p].r)
{
if(!tr[p].r||tr[tr[p].l].val>tr[tr[p].r].val)
{
zig(p);
remove(tr[p].r,key);
}
else
{
zag(p);
remove(tr[p].l,key);
}
}
else p=0;
}
else if(tr[p].key>key) remove(tr[p].l,key);
else remove(tr[p].r,key);
pushup(p);
}
int getRkByKey(int p,int key)
{
if(!p) return 0;
if(tr[p].key==key) return tr[tr[p].l].size+1;
if(tr[p].key>key) return getRkByKey(tr[p].l,key);
return tr[tr[p].l].size+tr[p].cnt+getRkByKey(tr[p].r,key);
}
int getKeyByRk(int p,int rk)
{
if(!p) return Inf;
if(tr[tr[p].l].size>=rk) return getKeyByRk(tr[p].l,rk);
if(tr[tr[p].l].size+tr[p].cnt>=rk) return tr[p].key;
return getKeyByRk(tr[p].r,rk-tr[tr[p].l].size-tr[p].cnt);
}
int getPrev(int p,int key)
{
if(!p) return -Inf;
if(tr[p].key>=key) return getPrev(tr[p].l,key);
return max(tr[p].key,getPrev(tr[p].r,key));
}
int getPostv(int p,int key)
{
if(!p) return Inf;
if(tr[p].key<=key) return getPostv(tr[p].r,key);
return min(tr[p].key,getPostv(tr[p].l,key));
}
int main()
{
build();
scanf("%d",&n);
while(n--)
{
int op,x;
scanf("%d%d",&op,&x);
if(op==1) insert(root,x);
else if(op==2) remove(root,x);
else if(op==3) printf("%d\n",getRkByKey(root,x)-1);//因为加了哨兵,所以实际排名得-1
else if(op==4) printf("%d\n",getKeyByRk(root,x+1));
else if(op==5) printf("%d\n",getPrev(root,x));
else printf("%d\n",getPostv(root,x));
}
}
2.Splay
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
struct Node
{
int s[2],p,v;
int size,flag;
void init(int _v,int _p)
{
v=_v,p=_p;
size=1;
}
}tr[maxn];
int n,m;
int root,idx;
void pushup(int x)
{
tr[x].size=tr[tr[x].s[0]].size+tr[tr[x].s[1]].size+1;
}
void pushdown(int x)
{
if(tr[x].flag)
{
swap(tr[x].s[0],tr[x].s[1]);
tr[tr[x].s[0]].flag^=1;
tr[tr[x].s[1]].flag^=1;
tr[x].flag=0;
}
}
void rotate(int x)
{
int y=tr[x].p,z=tr[y].p;
int k=tr[y].s[1]==x;
tr[z].s[tr[z].s[1]==y]=x,tr[x].p=z;
tr[y].s[k]=tr[x].s[k^1],tr[tr[x].s[k^1]].p=y;
tr[x].s[k^1]=y,tr[y].p=x;
pushup(y),pushup(x);
}
void splay(int x,int k)
{
while(tr[x].p!=k)
{
int y=tr[x].p,z=tr[y].p;
if(z!=k)
{
if((tr[y].s[1]==x)^(tr[z].s[1]==y)) rotate(x);
else rotate(y);
}
rotate(x);
}
if(!k) root=x;
}
void insert(int v)
{
int u=root,p=0;
while(u) p=u,u=tr[u].s[v>tr[u].v];
u=++idx;
if(p) tr[p].s[v>tr[p].v]=u;
tr[u].init(v,p);
splay(u,0);
}
int get_k(int k)
{
int u=root;
while(1)
{
pushdown(u);
if(tr[tr[u].s[0]].size>=k) u=tr[u].s[0];
else if(tr[tr[u].s[0]].size+1==k) return u;
else k-=tr[tr[u].s[0]].size+1,u=tr[u].s[1];
}
return -1;
}
void output(int u)
{
pushdown(u);
if(tr[u].s[0]) output(tr[u].s[0]);
if(tr[u].v>=1&&tr[u].v<=n) printf("%d ",tr[u].v);
if(tr[u].s[1]) output(tr[u].s[1]);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<=n+1;i++) insert(i);
while(m--)
{
int l,r;
scanf("%d%d",&l,&r);
l=get_k(l),r=get_k(r+2);
splay(l,0),splay(r,l);
tr[tr[r].s[0]].flag^=1;
}
output(root);
}
三、基础算法
1.回溯
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
2.二分与三分
①二分答案
(1)尽量往左找目标
while (l < r)
{
int mid = l + r >> 1; //(l+r)/2
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
(2)尽量往右找目标
while (l < r)
{
int mid = l + r + 1 >> 1; //(l+r+1)/2
if (check(mid)) l = mid;
else r = mid - 1;
}
(3)浮点二分
while(r-l>1e-5) //需要一个精度保证
{
double mid = (l+r)/2;
if(check(mid)) l=mid; //或r=mid;
else r=mid; //或l=mid;
}
②三分法
如果需要求出单峰函数的极值点,通常使用二分法衍生出的三分法求单峰函数的极值点。
while(l+eps<r)//三分法找最可能出错的值,即最小的x
{
mid1=l+(r-l)/3;
mid2=r-(r-l)/3;
//哪边更小哪边留下,另一边该为对应mid
if(f(mid1)<f(mid2)) r=mid2;
else l=mid1;
}
3.前缀和与差分
①前缀和
(1)一维前缀和
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
(2)二维前缀和
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
②差分
(1)一维差分
B[l]=a[l]-a[l-1];
给区间[l, r]中的每个数加上c:B[l] += c, B[r + 1] -= c
(2)二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
四、数学
1.高精度
①加法
// C = A + B, A >= 0, B >= 0
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(t);
return C;
}
②减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); i ++ )
{
t = A[i] - t;
if (i < B.size()) t -= B[i];
C.push_back((t + 10) % 10);
if (t < 0) t = 1;
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
③乘法
// C = A * b, A >= 0, b >= 0
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || t; i ++ )
{
if (i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
④除法
// A / b = C ... r, A >= 0, b > 0
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;
r = 0;
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
2.分解质因数
①试除法
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
int s = 0;
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
3.筛质数
①线性筛法
int p[maxn],vis[maxn];
int cnt=0;
void is_prime (int n)
{
for (int i = 2; i <= n; ++i)
{
if (!vis[i])
p[++cnt] = i;
for (int j = 1; j <= cnt; ++j)
{
if (i * p[j] > n)
break;
vis[i * p[j]] = 1; //p[j]是i*p[j]的最小素因子
if (i % p[j] == 0) //再往后p[j]就不是最小素因子了。因为这个时候开始,i可以分解成p[j]*另一个质因子,这个质因子比p[j]小从而代替p[j]称为i*p[j]的最小素因子
break;
}
}
}
4.最大公约数与最小公倍数
①欧几里得算法求最大公约数
ll gcd(ll a,ll b)
{
if(b)
{
return gcd(b,a%b);
}
else
{
return a;
}
}
②最小公倍数
res=a*b/gcd(a,b);
5.同余方程-扩展欧几里得算法
int ex_gcd(int a,int b,int& x,int& y)
{
if(b==0)
{
x=1;
y=0;
return a;
}
int ans=ex_gcd(b,a%b,x,y);
int tmp=x;//x2
x=y;//对应之前解释中的④式,x1=y2
y=tmp-a/b*y;//y2=x2-(a/b)*y2,切记a,b不是一直等于原线性同余方程的a,b,只有最开始的扩欧函数的才是
return ans;
}
int main()
{
x=a,y=b;
ex_gcd(a,b,x,y);
x=(x%b+b)%b;//x=(x0%t+t)%t,t=b/gcd(a,b),由于题目说ax ≡ 1 (mod b),则方程有整数解需1是gcd(a,b)的倍数,那显然gcd(a,b)=1,t=b,由于b>0,此时x必为最小正整数解
}
6.快速幂
//快速幂
ll fastPower(ll base, ll pow) {
ll res = 1;
while (pow > 0) {
if (pow & 1)//if(pow%2==1)
{
res = res * base;
}
pow >>= 1;//pow=pow/2
base = (base * base);
}
return res;
}
7.乘法逆元
①费马小定理
若mod为质数,则有a逆元即a(-1) % mod=1/a % mod=a(mod-2)
//示例:求组合数
typedef long long ll;
const int mod=10007;
ll fastPow(ll base,ll pow)
{
ll res=1;
base=base%mod;
while(pow>0)
{
if(pow&1)
{
res=res*base%mod;
}
pow>>=1;
base=(base*base)%mod;
}
return res;
}
int main()
{
int a,b,k,n,m;
cin>>a>>b>>k>>n>>m;
int res;
res=fastPow(a,n)*fastPow(b,m)%mod;
for(int i=1,j=0;i<=n,j<=m-1;i++,j++)
{
res=res*(k-j)%mod;//计算k(k-1)...(k-m+1)=k(k-1)...(k-(m-1))
res=res*fastPow(i,mod-2)%mod;//计算(1/i)%mod,用费马小定理
}
}
8.欧拉函数
欧拉函数φ(n)表示的是小于等于n和n互质的数的个数。比如φ(1)=1 [1],φ(4)=2 [1、3]。
int p[maxn],cnt;
int vis[maxn];
int phi[maxn];
void getEuler(int n)//写之前可先把欧拉筛写出来,然后去改动
{
phi[1]=1;//1的欧拉函数值等于1
for(int i=2;i<=n;i++)
{
if(!vis[i])
{
p[++cnt]=i;
phi[i]=i-1;//质数的欧拉函数值等于该数-1
}
for(int j=1;j<=cnt;j++)
{
if(i*p[j]>n)
{
break;
}
vis[i*p[j]]=1;
if(i%p[j]==0)//i有因子为p[j],p[j]不再是i*p[j]的最小素因子
{
phi[i*p[j]]=phi[i]*p[j];//根据推导出的公式可知,i*p[j]的欧拉函数值等于i的欧拉函数值乘以p[j]这个素数
break;
}
else
{
phi[i*p[j]]=phi[i]*phi[p[j]];//根据推导出的公式可知
}
}
}
}
9.博弈论
//先手必胜or先手必败
#include <bits/stdc++.h>
using namespace std;
const int maxn=2200,maxm=6600;
int n,m,k;
int h[maxn],e[maxm],ne[maxm],idx;
int f[maxn];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int sg(int u)
{
if(f[u]!=-1) return f[u];
unordered_set<int> s;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
s.insert(sg(j));
}
for(int i=0;;i++)
{
if(s.count(i)==0)
{
f[u]=i;
break;
}
}
return f[u];
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
memset(h,-1,sizeof h);
for(int i=0;i<m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
memset(f,-1,sizeof f);
int res=0;
for(int i=0;i<k;i++)
{
int u;
scanf("%d",&u);
res^=sg(u);
}
if(res) puts("win");
else puts("lose");
}
五、图论
1.最短路算法
①无权图的单源最短路算法:bfs
(1)边权都是0:普通队列
//以矩阵图为例
typedef long long ll;
typedef pair<int,int> pii;
const int Inf=0x3f3f3f3f;
const int maxn=440,maxm=2e4+2;
int dx[maxn*maxn],dy[maxn*maxn],cnt;//类似于之前固定的往上下左右四个方向跳1步的const规则dx,dy,这里是要你根据sqrt(M)限制生成跳的规则
int dist[maxn][maxn];
void bfs()
{
memset(dist,Inf,sizeof dist);
queue<pii> q;
q.push({1,1});
dist[1][1]=0;
while(!q.empty())
{
pii t=q.front();
q.pop();
int x=t.x,y=t.y;
rep(i,0,cnt)
{
int xx=x+dx[i],yy=y+dy[i];
if(xx<=0||yy<=0||xx>N||yy>N) continue;
if(dist[xx][yy]>dist[x][y]+1)
{
dist[xx][yy]=dist[x][y]+1;
q.push({xx,yy});
}
}
}
}
(1)边权有0有1:双端队列
int n;// 点的数量
int h[maxn], w[maxn], e[maxn], ne[maxn], idx;// 邻接表存储所有边
int dist[maxn];// 存储所有点到1号点的距离
int vis[maxn];// 存储每个点的最短距离是否已确定
void bfs(int s)
{
dist[s]=0;
deque<int> dq;//针对边权为0,1的边的图,做单源最短路想到用dq,正确性证明可参照堆优化dij
dq.push_back(s);
while(!dq.empty())
{
int t=dq.front();
dq.pop_front();
if(vis[t])
{
continue;
}
vis[t]=1;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
if(w[i])//权为1入队尾
{
dq.push_back(j);
}
else//权为0入队头
{
dq.push_front(j);
}
}
}
}
}
②有权图的单源最短路算法:dij
(1)朴素算法
//这里是求最大
int n,m;
double G[maxn][maxn];
double dist[maxn];//从源点开始到各点的最大距离,即最大剩余百分比
int collected[maxn];
int findMaxdist()
{
int maxv;
double maxdist=0;
for(int v=1;v<=n;v++)
{
if(!collected[v]&&dist[v]>maxdist)
{
maxv=v;
maxdist=dist[v];
}
}
if(maxdist==0)
{
return -1;
}
else
{
return maxv;
}
}
void dijkstra(int s)
{
for(int v=1;v<=n;v++)
{
dist[v]=G[s][v];
}
dist[s]=1;
while(1)
{
int v=findMaxdist();//找到剩余百分比最大的边
if(v==-1)
{
break;
}
collected[v]=1;
for(int w=1;w<=n;w++)
{
if(!collected[w]&&G[v][w]!=0)
{
if(dist[w]<dist[v]*G[v][w])//用更大的做松弛
{
dist[w]=dist[v]*G[v][w];
}
}
}
}
}
(2)堆优化算法
//邻接表版本
typedef pair<int, int> pii;
const int maxn=1e4+1;
const int Inf=0x3f3f3f3f;
int n;// 点的数量
int h[maxn], w[maxn], e[maxn], ne[maxn], idx;// 邻接表存储所有边
int dist[maxn];// 存储所有点到1号点的距离
int vis[maxn];// 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra(int s,int d)
{
memset(dist, Inf, sizeof dist);//若用的是形参的dist,则只能用fill初始化
dist[s] = 0;
priority_queue<pii, vector<pii>, greater<pii>> pq;
pq.push({dist[s], s});// first存储距离,second存储节点编号
while (!pq.empty())
{
auto t = pq.top();
pq.pop();
int v = t.second, dd = t.first;
if (vis[v]) continue;
vis[v] = 1;
for (int i = h[v]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dd + w[i])
{
dist[j] = dd + w[i];
pq.push({dist[j], j});
}
}
}
if (dist[d] == Inf) return -1;
return dist[d];
}
//邻接图版本
typedef pair<int,int> pii;
const int maxn=5e4+5;
const int Inf=0x3f3f3f3f;
struct Node
{
int index;
int dist;
bool operator < (const Node &a) const
{
return dist>a.dist;
}
};
int n,m;
priority_queue<Node> pq;
vector<pii> G[maxn];
int collected[maxn];
int dist[maxn];
void dijkstra(int s,i)
{
fill(collected,collected+maxn,0);
fill(dist,dist+maxn,Inf);
dist[s]=0;
pq.push({s,dist[s]});
while(!pq.empty())
{
int minv=pq.top().index;
pq.pop();
if(collected[minv])
{
continue;
}
collected[minv]=1;
for(int i=0;i<(int)G[minv].size();i++)
{
int v=G[minv][i].first;
int w=G[minv][i].second;
if(!collected[v]&&dist[v]>dist[minv]+w)
{
dist[v]=dist[minv]+w;
pq.push({v,dist[v]});
}
}
}
}
③有权图的单源最短路算法:spfa
const int maxn=2510,maxm=6200*2+10;
const int Inf=0x3f3f3f3f;
int n,m,s,t;
int h[maxn],e[maxm],w[maxm],ne[maxm],idx;
int dist[N];
int book[N];
void spfa()
{
queue<int> q;
fill(dist,dist+N,Inf);
dist[s]=0;
q.push(s);
book[s]=1;
while(!q.empty())
{
int t=q.front();
q.pop();
book[t]=0;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
if(!book[j])
{
q.push(j);
book[j]=1;
}
}
}
}
}
④有权图的多源最短路算法:floyd
//初始化图
fill(G[0],G[0]+maxn*maxn,Inf);
for(int i=1;i<=n;i++)//floyd初始化G的时候要注意把到自身的距离设置成0
{
G[i][i]=0;
}
//输入图
//floyd
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
G[i][j]=min(G[i][j],G[i][k]+G[k][j]);
}
}
}
2.最小生成树
①Kruskal
#include <bits/stdc++.h>
using namespace std;
const int maxk=220,maxn=110;
struct Edge
{
int u,v;
int w;
bool operator < (const Edge &e) const
{
return w<e.w;
}
}E[maxk];
int n,k;
int p[maxn];
int findRoot(int v)
{
if(p[v]<0)
{
return v;
}
else
{
return p[v]=findRoot(p[v]);
}
}
void unionSet(int v1,int v2)
{
int root1=findRoot(v1),root2=findRoot(v2);
if(root1==root2)
{
return;
}
if(p[root1]<p[root2])
{
p[root1]+=p[root2];
p[root2]=root1;
}
else
{
p[root2]+=p[root1];
p[root1]=root2;
}
return;
}
int Kruskal()
{
memset(p,-1,sizeof p);
int weightsum=0;
for(int i=0;i<k;i++)
{
int u=E[i].u,v=E[i].v,w=E[i].w;
int root1=findRoot(u),root2=findRoot(v);
if(root1==root2)
{
weightsum+=w;//没用来组成最小生成树的边就是除掉的边,把这些边的权求和即为答案
}
else
{
unionSet(u,v);
}
}
return weightsum;
}
int main()
{
cin>>n>>k;
for(int i=0;i<k;i++)
{
cin>>E[i].u>>E[i].v>>E[i].w;
}
sort(E,E+k);
cout<<Kruskal();
}
3.负环
①spfa
bool spfa()
{
fill(dist,dist+maxn,Inf);//根据实践证明此操作可省略,因为dist可以为任意值,因为本质是求负环,dist最后有负环一定为贼负
fill(cnt,cnt+maxn,0);
fill(book,book+maxn,0);
queue<int> q;
for(int i=1;i<=N;i++)
{
q.push(i);
book[i]=1;
}
while(!q.empty())
{
int t=q.front();
q.pop();
book[t]=0;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
cnt[j]=cnt[t]+1;//到达j的最短路径所包含的边数为到达t的路径的边数+当前这1条边
if(cnt[j]>=N)//当边数大于等于N顶点数则说明存在负环
{
return true;
}
if(!book[j])
{
q.push(j);
book[j]=1;
}
}
}
}
return false;
}
4.差分约束
//示例:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+10,maxm=3e5+30;
const int Inf=0x3f3f3f3f;
int N,K;
int h[maxn],e[maxm],w[maxm],ne[maxm],idx;
ll dist[maxn];
int cnt[maxn],book[maxn];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
bool spfa()//spfa判环
{
memset(dist,-Inf,sizeof dist);
memset(book,0,sizeof book);
memset(cnt,0,sizeof cnt);
stack<int> s;//将队列替换成栈,在这类带环的问题里头有奇效,效率会变高
dist[0]=0;
s.push(0);
book[0]=1;
while(!s.empty())
{
int t=s.top();
s.pop();
book[t]=0;
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(dist[j]<dist[t]+w[i])//最长路,往大的松弛
{
dist[j]=dist[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=N+1)//因为多设置了一个虚拟源点0,所以点数共有N+1,则路径边数最大是N,超过必成环
{
return false;
}
if(!book[j])
{
s.push(j);
book[j]=1;
}
}
}
}
return true;
}
int main()
{
scanf("%d%d",&N,&K);
memset(h,-1,sizeof h);
for(int i=0;i<K;i++)
{
int x,a,b;
scanf("%d%d%d",&x,&a,&b);
if(x==1) add(b,a,0),add(a,b,0);//每一个不等式对应一种连边方式
else if(x==2) add(a,b,1);
else if(x==3) add(b,a,0);
else if(x==4) add(b,a,1);
else add(a,b,0);
}
for(int i=1;i<=N;i++)//建立虚拟源点,连边用于表示那个绝对关系的不等式
{
add(0,i,1);
}
if(!spfa())
{
printf("-1\n");
}
else
{
ll res=0;
for(int i=1;i<=N;i++)
{
res+=dist[i];//总共至少要多少,肯定是要求和一波
}
printf("%lld\n",res);
}
}
5.lca
①倍增算法
#include <bits/stdc++.h>
using namespace std;
const int maxn=4e4+10;
struct Node
{
int to;
int w;
Node(int a,int b)
{
to=a;
w=b;
}
};
vector<Node> G[maxn];
int fa[maxn][17];
int d[maxn];
int dis[maxn];
int vis[maxn];
int root;
void init()
{
fill(fa[0],fa[0]+maxn*17,0);
fill(vis,vis+maxn,0);
fill(d,d+maxn,0);
fill(dis,dis+maxn,0);
}
void dfs(int v0)//dfs得到各节点的深度以及各节点到根的距离
{
vis[v0]=1;
for(int i=0;i<G[v0].size();i++)
{
int v=G[v0][i].to;
if(!vis[v])
{
d[v]=d[v0]+1;
dis[v]=dis[v0]+G[v0][i].w;//v到根节点的距离等于v0到根节点的距离加v0与v之间的距离
dfs(v);
}
}
}
void getFa(int n)
{
for(int j=1;j<16;j++)
{
for(int i=1;i<=n;i++)
{
//显然i节点往上跳2^j层所到达的节点等于i节点往上跳2^(j-1)层所到达的节点往上跳2^(j-1)层所到达的节点
//如果理解不过来可以特例代入理解
fa[i][j]=fa[fa[i][j-1]][j-1];
}
}
}
int lca(int u,int v)
{
if(d[u]<d[v])
{
swap(u,v);//保证深度较大者为u,便于操作
}
int dd=d[u]-d[v];//深度差
for(int i=0;i<=16;i++)//将u跳到和v相同的层
{
//0011=3=1+2
//0001=1
//0010=2,跳三层可通过跳1+2层实现
if((1<<i)&dd)
{
u=fa[u][i];
}
}
if(u==v)
{
return u;
}
for(int i=16;i>=0;i--)//从高位开始跳,比如跳3层,应先跳2层再跳1层,都是2的次幂的跳
{
//cout<<u<<" "<<v<<endl;
if(fa[u][i]!=fa[v][i])//在提到相同层的基础上开始同时往上跳,跳到fa相同为止
{
u=fa[u][i];
v=fa[v][i];
}
}
//cout<<u<<" "<<v<<endl;
u=fa[u][0];//因为是跳到u和v的fa相同为止,显然此时再往上跳一层就是lca了
return u;
}
int main()
{
cin.tie(0),cout.tie(0);
int T;
cin>>T;
for(int k=0;k<T;k++)
{
init();
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
G[i].clear();
}
for(int i=0;i<n-1;i++)
{
int u,v,w;
cin>>u>>v>>w;
G[u].push_back(Node{v,w});
fa[v][0]=u;//表示节点v往上跳2^0层所到达的节点
if(fa[u][0]==0)
{
root=u;
}
//cout<<root<<endl;
}
d[root]=1;
dfs(root);
getFa(n);
for(int i=0;i<m;i++)
{
int v1,v2;
cin>>v1>>v2;
int ans=dis[v1]+dis[v2]-2*dis[lca(v1,v2)];//树上两点间距离等于两点到根的距离之和减去二者最近公共祖先到根的距离的两倍
cout<<ans<<endl;
}
}
}
6.有向图的强连通分量
void tarjan(int u)
{
dfn[u]=low[u]=++timestamp;
s.push(u);is_s[u]=1;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(is_s[j])
{
low[u]=min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u])
{
++scc_cnt;
int y;
do{
y=s.top();
s.pop();
is_s[y]=0;
id[y]=ssc_cnt;
}while(y!=u);
}
}
7.二分图
①染色法
int n; // n表示点数
int h[N], e[M], ne[M], idx; // 邻接表存储图
int color[N]; // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色
// 参数:u表示当前节点,c表示当前点的颜色
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (color[j] == -1)
{
if (!dfs(j, !c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
bool check()
{
memset(color, -1, sizeof color);
bool flag = true;
for (int i = 1; i <= n; i ++ )
if (color[i] == -1)
if (!dfs(i, 0))
{
flag = false;
break;
}
return flag;
}
②最大匹配
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res++;
}
Tip:最大匹配数 = 最小点覆盖 = 总点数-最大独立集 = 总点数-最小路径覆盖
8.拓扑排序
void topSort()
{
queue<int> q;
for(int i=1;i<=n;i++)//将所有度为0的节点先入队
{
if(!d[i])
{
q.push(i);
}
}
while(!q.empty())
{
int t=q.front();
q.pop();
cout<<t<<" ";
for(int i=h[t];i!=-1;i=ne[i])
{
int j=e[i];
if(--d[j]==0)//将输出的节点的邻接点的度数-1,看度数是否为0,如果为0则入队
{
q.push(j);
}
}
}
}
六、动态规划
1.背包问题
1.01背包问题
//不求具体方案
int N,V;
int dp[maxn];
int main()
{
cin>>N>>V;
for(int i=0;i<N;i++)
{
int v,w;
cin>>v>>w;
for(int j=V;j>=v;j--)
{
dp[j]=max(dp[j],dp[j-v]+w);
}
}
cout<<dp[V];
}
//求具体方案
int v[maxn],w[maxn];
int dp[maxn][maxn];
int main()
{
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)
{
cin>>v[i]>>w[i];
}
for(int i=N;i>=1;i--)//倒着遍历物品,从而使得最大价值出现在dp[1][V],这样打印方案的时候,才能因为按照字典序从而得从前向后遍历,而使得最前面的价值为应定的总价值
{
for(int j=0;j<=V;j++)
{
//因为是倒着遍历,所以是用i+1来更新i
dp[i][j]=dp[i+1][j];
if(j-v[i]>=0)
{
dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
}
}
}
int j=V;
for(int i=1;i<=N;i++)//正着遍历物品,使得结果序字典序排
{
if(j>=v[i]&&dp[i][j]==dp[i+1][j-v[i]]+w[i])//可选(j>=v[i]),必须选(dp[i][j]的值等于此前(因为是从后往前推,所以是i+1)的价值加上这个物品价值的结果)
{
cout<<i<<" ";
j-=v[i];//剩余体积减少
}
}
}
2.二维花费的01背包问题
int dp[maxn][maxn];
int main()
{
int N,M,K;
cin>>N>>M>>K;
for(int i=0;i<K;i++)
{
int v1,v2,w;
cin>>v1>>v2>>w;
for(int j=N;j>=v1;j--)//j遍历球的数量(背包容量1)
{
for(int k=M;k>=v2;k--)//k遍历体力的多少(背包容量2)
{
dp[j][k]=max(dp[j][k],dp[j-v1][k-v2]+w);
}
}
}
cout<<dp[N][M];
}
3.混合背包(内置多重背包、完全背包)
#include <bits/stdc++.h>
using namespace std;
const int maxn=1010;
int dp[maxn];
int main()
{
int N,V;
cin>>N>>V;
for(int i=0;i<N;i++)
{
int v,w,s;
cin>>v>>w>>s;
if(s==-1)
{
s=1;
}
if(s==0)
{
for(int j=v;j<=V;j++)
{
dp[j]=max(dp[j],dp[j-v]+w);
}
}
else
{
//二进制优化计算多重背包,此时只能先遍历物品个数(6=1+2+3,8=1+2+4+1,...),再遍历容量(其实就是把k增加方式变了一下而已)
for(int k=1;k<=s;k<<=1)
{
for(int j=V;j-k*v>=0;j--)
{
dp[j]=max(dp[j],dp[j-k*v]+k*w);
}
s-=k;//拆分
}
if(s)//二进制拆分剩下的数-物品个数,再来一次遍历背包容量状态转移
{
for(int j=V;j-s*v>=0;j--)
{
dp[j]=max(dp[j],dp[j-s*v]+s*w);
}
}
}
}
cout<<dp[V];
}
4.分组背包问题
//无依赖
int a[maxn],b[maxn];//物品体积,价值
vector<int> g[maxn];//g[i]存第i组的物品输入时的编号
int cnt;//记最大组
int dp[maxn];//dp[i]表示容量为i时所能获得的最大价值
int main()
{
int n,m;
cin>>m>>n;//背包容量,物品个数
for(int i=0;i<n;i++)
{
cin>>a[i]>>b[i];
int c;
cin>>c;
cnt=max(cnt,c);
g[c].push_back(i);
}
for(int k=1;k<=cnt;k++)//k遍历组
{
for(int i=m;i>=0;i--)//i遍历背包容量
{
for(int j=0;j<g[k].size();j++)//j遍历k组物品
{
int t=g[k][j];//获得物品编号
if(i-a[t]>=0)
{
dp[i]=max(dp[i],dp[i-a[t]]+b[t]);//参考01背包状态转移
}
}
}
}
cout<<dp[m];
}
//有依赖
int v[maxm],w[maxm],q[maxm];//价格(体积),物品实际价值,主附件情况
vector<int> g[maxm];//g[i]存i组主件配的所有附件
int dp[maxn];//dp[i]表示手头上钱为i时所能拥有的最大物品价值
int main()
{
int N,m;
cin>>N>>m;
for(int i=1; i<=m; i++)
{
int p;
cin>>v[i]>>p>>q[i];
w[i]=v[i]*p;//v*p为实际价值
g[q[i]].push_back(i);
}
for(int k=1; k<=m; k++)//k遍历组
{
for(int i=N; i>=0; i--)//i遍历(手头的钱)背包容量
{
//二进制枚举,用于简化代码
for(int j=0; j<(1<<g[k].size()); j++)//2^(g[k].size())可表示由该组内各种物品组成的集合(分组背包问题是要求分组里头的物品只能选一个,但是这里却可以选多个,所以我们这里就要通过二进制化成选法的集合的方式来使得每个选法当作一个物品来选)
{
int nv=0,nw=0;
if(!q[k])//当前组为主件组
{
nv=v[k],nw=w[k];//算上主件信息
}
else//为附件就break,不用遍历那堆集合(因为本来就没有)
{
break;
}
for(int t=0;t<g[k].size();t++)
{
if(j&(1<<t))
{
nv+=v[g[k][t]];
nw+=w[g[k][t]];
}
}
if(i-nv>=0)
{
dp[i]=max(dp[i],dp[i-nv]+nw);//用nv,nw做状态转移
}
}
}
}
cout<<dp[N];
}
5.有依赖的背包问题
int N,V;
vector<int> G[maxn];
int v[maxn],w[maxn];
int root;
int dp[maxn][maxn];
int dfs(int x)
{
for(int i=v[x];i<=V;i++)//先用必选的x更新当前状态
{
dp[x][i]=w[x];
}
for(int i=0;i<G[x].size();i++)
{
int y=G[x][i];
dfs(y);//进入dfs深入,之后子节点的集合已被计算完成,可供当前节点使用
for(int j=V;j-v[x]>=0;j--)//保证当前根x能够被选,如果j-v[x]<0,则容量不够了
{
for(int k=0;(j-v[x])-k>=0;k++)//保证给子物品的空间k比j-v[x]来的小,否则当前根x放不了了
{
dp[x][j]=max(dp[x][j],dp[x][j-k]+dp[y][k]);//用选根x,并腾出了k空间的价值与选当前孩子y,用上腾出的空间k的价值的和来尝试更新状态
}
}
}
}
int main()
{
cin>>N>>V;
for(int i=1;i<=N;i++)
{
int fa;
cin>>v[i]>>w[i]>>fa;
if(fa==-1)
{
root=i;
}
G[fa].push_back(i);
}
dfs(root);
cout<<dp[root][V];
}
2.状态机模型
//以带冷冻期的股票买卖问题为例
int N;
int w[maxn];
int dp[maxn][3];
//dp[i][0]表示第i天持股时可获得的最大价值,dp[i][1]表示第i天已未持股1天可获得的最大价值,dp[i][2]表示第i天已未持股2天以及以上可获得的最大价值
int main()
{
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>w[i];
}
dp[0][0]=dp[0][1]=-Inf;//开始未持股,也不能说未持股一天,所以对应dp[0][0],dp[0][1]初始化未负无穷表示不可达
dp[0][2]=0;//第0天未持股2天及以上根据理解显然可初始化为0
for(int i=1;i<=N;i++)//遍历天
{
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-w[i]);//第i天持股可由第i-1天也持股或者第i-1天未持股2天及以上中取最大得到
dp[i][1]=dp[i-1][0]+w[i];//第i天未持股1天只能由第i-1天持股通过第i天卖掉股票得到
dp[i][2]=max(dp[i-1][1],dp[i-1][2]);//第i天未持股2天及以上可由第i-1天未持股1天或者第i-1天持股2天及以上取最大得到
}
cout<<max(dp[N][1],dp[N][2]);
}
3.状压dp
//小国王问题-n*n棋盘摆放可以攻击相邻8个格子的棋子放k个使得相互无法攻击的方案数
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;//防止数据溢出
const int maxn=12,maxm=1<<10,maxk=110;
//n最大其实是10,但是后期要用到11行,所以maxn就整到了12,m表示状态个数,显然由于1行是10列,所以状态个数最多有2^10个,maxk为最大棋数
int N,K;
vector<int> state;//存储所有合法状态,对应棋盘上的一行,相邻之间不能同时为1
vector<int> head[maxm];//head[i]存储i下标对应的合法状态的上一个合法状态对应的下标
int cnt[maxm];//cnt[i]表示表示下标为i的合法状态所含的1的个数,即棋子的个数
ll dp[maxn][maxk][maxm];//dp[i][j][a]表示放到第i行,共用了j个棋子,且最后一行对应的状态为下标为a的合法状态下对应的方案数
int check(int state)//检查状态state每相邻两个数是否同时为1
{
for(int i=0;i<N-1;i++)//最多移动N-1位(i+1=N-1)
{
//右移i位直接将state这个数表示的二进制状态中第i位上的数移动到了最右边,与上1后如果等于1则第i位上数为1否则为0
if((state>>i&1)&&(state>>i+1&1))//循环直接看第i位和第i+1位
{
return 0;
}
}
return 1;
}
int count(int state)//统计该状态对应的1的个数,即棋子的个数
{
int res=0;
for(int i=0;i<N;i++)
{
res+=state>>i&1;
}
return res;
}
int main()
{
cin>>N>>K;
//1.预处理
for(int i=0;i<1<<N;i++)//枚举到2^N
{
if(check(i))//判断状态是否合法
{
state.push_back(i);//存入合法状态
cnt[i]=count(i);//记录该合法状态1的个数
}
}
for(int i=0;i<state.size();i++)
{
for(int j=0;j<state.size();j++)//i,j遍历每个合法状态
{
int a=state[i],b=state[j];
if((a&b)==0&&check(a|b))//看b到a(上->下)是否可达,两种状态对应的二进制数每一位对应数不能同时为1,且相邻位不能同时为1
{
head[i].push_back(j);//j->i
}
}
}
//2.dp
dp[0][0][0]=1;//显然放到第0行,放了0个棋子,状态为下标0对应的合法状态(其实就是0)方案数为1
for(int i=1;i<=N+1;i++)//i遍历行数,有意遍历到N+1,使得答案能够被存入N+1行
{
for(int j=0;j<=K;j++)//j遍历棋子个数
{
for(int a=0;a<state.size();a++)//a遍历该行状态
{
for(int b:head[a])//b为该状态对应的上一层状态
{
int num=cnt[state[a]];//该行状态对应的1的个数
if(j>=num)//这1的个数当然不能超过棋子的个数
{
dp[i][j][a]+=dp[i-1][j-num][b];//a由b状态转移过来,b处于i-1行,且那个时候用了j-num个棋子
}
}
}
}
}
cout<<dp[N+1][K][0];//N+1行状态为什么棋子也不放,且那个时候共使用了K个棋子,对应的方案数就是答案
}
//最少抛物线点覆盖问题
#include <bits/stdc++.h>
#define x first
#define y second
using namespace std;
typedef pair<double,double> pdd;
const int maxn=18,maxm=1<<18;
const double eps=1e-8;
int n,m;
pdd p[maxn];//点
int path[maxn][maxn];//path[i][j]:表示经过pi,pj两点的抛物线能覆盖的点对应的状态
int dp[maxm];//dp[i]表示覆盖的点达到i状态对应的最小抛物线数
int cmp(double x,double y)//浮点数比较
{
if(fabs(x-y)<eps) return 0;//相等
if(x<y) return -1;
return 1;
}
int main()
{
int T;
cin>>T;
while(T--)
{
cin>>n>>m;
for(int i=0;i<n;i++) cin>>p[i].x>>p[i].y;//初始化点
//预处理path数组
memset(path,0,sizeof path);
for(int i=0;i<n;i++)//枚举点
{
path[i][i]=1<<i;//抛物线只经过p1
for(int j=0;j<n;j++)//枚举点
{
double x1=p[i].x,y1=p[i].y;//p1
double x2=p[j].x,y2=p[j].y;//p2
if(!cmp(x1,x2)) continue;
double a=(y1/x1-y2/x2)/(x1-x2);//根据公式计算a、b
double b=y1/x1-a*x1;
if(cmp(a,0)>=0) continue;//抛物线需a<0,开口向下
int state=0;
for(int k=0;k<n;k++)//得到经过p1、p2的抛物线经过的点对应的状态
{
double x=p[k].x,y=p[k].y;
if(!cmp(a*x*x+b*x,y)) state+=1<<k;//ax^2+bx==y则经过
}
path[i][j]=state;
}
}
//状压dp,计算最终状态的属性值:用到的抛物线数
memset(dp,0x3f,sizeof dp);//求最小,初始化为无穷
dp[0]=0;//一个点都没覆盖,显然最少用到的抛物线数为0
for(int i=0;i<(1<<n)-1;i++)//状态到全1,即所有点都覆盖,因此i需小于2^n-1
{
int x=0;
for(int j=0;j<n;j++)//随便找到i状态中没有被覆盖的点px
{
if(!(i>>j&1))
{
x=j;
break;
}
}
for(int j=0;j<n;j++)//尝试覆盖px,更新dp数组
{
dp[i|path[x][j]]=min(dp[i|path[x][j]],dp[i]+1);//每次都是用一个新的抛物线(覆盖px,pj的抛物线)来覆盖px
}
}
cout<<dp[(1<<n)-1]<<endl;//全1状态时即所有点都覆盖了,输出结果值
}
}
4.LIS
//最长上升子序列
int res=0
for(int i=1;i<=N;i++)
{
dp[i]=1;//固定最后一个选择,以h[i]结尾,然后考虑之前的操作
for(int j=1;j<i;j++)
{
if(h[i]>h[j]) dp[i]=max(dp[i],dp[j]+1);//之前的操作为以j对应元素结尾的上升子序列的最大长度,加上当前h[i]就是dp[j]+1
}
res=max(res,dp[i]);
}
//最长公共子序列
#include <bits/stdc++.h>
using namespace std;
const int maxn=3030;
int a[maxn],b[maxn];
int dp[maxn][maxn];
int main()
{
int N;
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
for(int i=1;i<=N;i++)
{
cin>>b[i];
}
for(int i=1;i<=N;i++)
{
int maxv=1;
for(int j=1;j<=N;j++)//k
{
dp[i][j]=dp[i-1][j];
if(a[i]>b[j])//相当于原来的a[i]>b[k],此时k介于1到N,如何保证枚举仍然在1~j?由于a[i]=b[j],我们条件是a[i]>b[k],即b[j]>b[k],显然由于b[j]>b[k],这是一个上升子序列,满足j>k,所以k不会枚举到j
{
maxv=max(maxv,dp[i-1][j]+1);
}
if(a[i]==b[j])
{
dp[i][j]=max(dp[i][j],maxv);
}
}
}
int res=0;
for(int i=1;i<=N;i++)
{
res=max(res,dp[N][i]);
}
cout<<res;
}
5.区间dp
//能量项链
const int maxn=110;
int w[2*maxn];
int dp[2*maxn][2*maxn];//dp[l][r]表示第l个珠子到第r个珠子合并所能获得的最大能量
int main()
{
int N;
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>w[i];
w[i+N]=w[i];//环拉成链
}
for(int len=3;len<=N+1;len++)//枚举区间长度:为什么len从3开始?以本样例为例,设len=3可有2、3、5,即对应(2,3)、(3,5),否则会少一个珠子则无法合并;为什么len最大为N+1,因为r=l+len-1最大可到2*N,即作为最后一颗珠子的尾标记,则=>len最大可到N+1
{
for(int l=1;l+len-1<=2*N;l++)//枚举左端点
{
int r=l+len-1;//确定右端点
for(int k=l+1;k<r;k++)//枚举中间点(矩阵1的1列数,矩阵2的行数)
{
dp[l][r]=max(dp[l][r],dp[l][k]+dp[k][r]+w[l]*w[k]*w[r]);//切记这里k是第一个矩阵的列,第二个矩阵的行,即[l][k],[k][r],和之前石子合并分成两段区间不大一样,那个是[l][k],[k+1][r]
}
}
}
int res=0;
for(int i=1;i<=N;i++)//枚举左端点,长度为N,确定最大值
{
res=max(res,dp[i][i+N]);
}
cout<<res;
}
6.数位dp
//求给定区间 [X,Y] 中满足下列条件的整数个数:这个数恰好等于K个互不相等的B的整数次幂之和。
#include <bits/stdc++.h>
using namespace std;
const int maxn=35;//B进制最小为2,则由题目给的X、Y数据范围maxn设为>32
int K,B,l,r;
int f[maxn][maxn];//f[i][j]表示i个数里头选j个数出来的方案数,即组合数C(i,j)
//求组合数
void init()
{
for(int i=0;i<maxn;i++)
{
for(int j=0;j<=i;j++)
{
if(!j) f[i][j]=1;//i个数里头选0个数的方案数为1
else f[i][j]=f[i-1][j]+f[i-1][j-1];//C(i,j)=C(i-1,j-1)+C(i-1,j)
}
}
}
int dp(int n)//求<=n的满足性质的数的个数
{
if(!n) return 0;//
//分离出在B进制下n的各个位
vector<int> nums;
while(n) nums.push_back(n%B),n/=B;
int res=0;//方案数
int last=0;//存储之前用过的1的个数
for(int i=nums.size()-1;i>=0;i--)//从高位向低位枚举B进制下n的各个位
{
int x=nums[i];
if(x>0)//x>0时才有左分支(填0到x-1范围内的数,但注意本题只能填0到1)
{
res+=f[i][K-last];//左分支,当前位填0,则方案数加上从i位中挑K-last个位来填1的方案数
if(x>1)//左分支,当前位要填1,必须x>1
{
if(K-last-1>=0) res+=f[i][K-last-1];//当前位填1,则方案数加上从i位中挑K-last-1个位来填1的方案数
break;//右分支,当前位填x,则必定不满足数性质(它是只能填0、1),直接break
}
else//x==1则左分支,当前位只能填0
{
last++;//考虑右分支,当前位填x,即1,则之前用过的1的个数++
if(last>K) break;//之前用过的1的个数超过了能够填1的个数直接break
}
}
if(!i&&last==K) res++;//遍历到最低位,且最低一位只能为0(上面操作考虑过了该位填1的情况),此时res++
}
return res;
}
int main()
{
init();//预处理组合数
cin>>l>>r>>K>>B;
cout<<dp(r)-dp(l-1);//f(X,Y)=f(Y)-f(X-1)
}