ACM学习笔记
随时思考或者思路简录
-
n>10^5, 定义为全局变量
-
绝对值函数:
abs()
向上取整函数:ceil()
-
1LL*的用法
-
可以使用并查集来判断一个图中是否存在环
-
初始化数组最大值
memset(a,0x3f,sizeof(a))
-
数字最大
const int maxn=0x3f3f3f3f
; -
不开 long long 见祖宗
-
字符串变数组 atoi(s)
-
求开n次方?
pow(x,1.0/n)
首先x>0,x y太大用 double ,double的范围是10的308次方
文章目录
- ACM学习笔记
- 0.acm输入的几种情况:
- 1.c++STL
- 2.动态规划
- 3.快速幂
- 4.记忆化搜索
- 5.高精度
- 6.手动二分
- 7.尺取
- 8.并查集
- 9.倍增RMQ(区间最值查询)
- 10.图和树的存储和遍历
- 11.排序
- 12.递推递归
- 13.简单数学与简约枚举
- 14.位运算
- 15深度优先搜索
- 16广度优先搜索
- 17.树状数组和线段树
- 18.[最短路径](最短路径讲义.md) [wiki](https://cuccs.github.io/acm-wiki/graph/shortest-path/)
- 19拓扑排序和强连通分量
- 20[最小生成树](https://cuccs.github.io/acm-wiki/graph/MST/)
- 21[二分图、二分图最大匹配](https://cuccs.github.io/acm-wiki/graph/bipartite-graph/)
- 22[LCA ](LCA 最近公共祖先讲义.md)
- 23树上差分
- 24状态压缩DP、区间DP
- 25数论
- 26trie,kmp,字符哈希
- 27博弈基础
- 28背包DP,树上DP (树上背包)
0.acm输入的几种情况:
1.输入包含若干组整数a,b,a,b之间通过空格隔开,每一组数据占一行。
while(scanf("%d %d",&a,&b)!=EOF){
}
2.第一行为一个整数N,紧接着是N行数据,每行包含有空格隔开的两个整数a,b,每组数据占一行
scanf("%d",&N);
while(N--){
scanf("%d %d",&a,&b);//读入每组需要处理的数据
}
3.输入若干组整数a,b,a b之间通过空格隔开,每组数据占一行。当输入为0 0时,输入结束,本组数据不需要处理。
while(scanf("%d %d",&a,&b)&&(a!=0||b!=0)){
}
4.输入包含若干组测试数据。每组数据只占一行,每行包含一个整数N及其后紧跟着N个整数。以0开头的测试数据表示输入结束,本组数据不需要处理。
while(scanf("%d %d",&a,&b)&&N){
while(N--){
scanf("%d",&element);
}
}
1.c++STL
1.string
- 读一行:
getline(cin,名字);
memset(str,0,sizeof(str))
暴力清空 逐个字节清空填充 也可初始化-1- 字符串的长度
s.length()
- 首尾
s.begin(),s.end()
2.ctime
time(0) 1970到现在的秒数
//srand(timo(0));
#include<ctime>
srand(unsigned int)time(NULL));//随机数种子
int random=rand()%61+40;
3.sort排序
-
头文件
algorithm
-
**sort 可以排序 数组 字符串string(字典序) 结构体 vector **
-
sort(数组名+n1,数组名+n2)
左闭右开 从小到大 -
sort(数组名+n1,数组名+n2,greater<T>)
从大到小 T是数据类型 -
sort(数组名+n1,数组名+n2,排序规则结构名())
struct 结构名
{
bool operator()(const T & a1,const T & a2)const {
//a1应该在a2前,返回 true
//否则返回 false
}
}
6. sort函数的bool写法。甚至可以给结构体排序
sort(a,a+n,cmp);
bool cmp(xuesheng a,xuesheng b){
if(a.sum ==b.sum ) return a.k <b.k ;
else return a.sum >b.sum ;
}
4.二分查找lower_bound
排好序的数组 : 查找规则必须和排序规则一致
binary_search(数组名+n1,数组名+n2,值) - 数组名
查找等于(!a>b&&!a<b—a排在b前后都行)值的元素,找到返回true,否则falselower_bound(数组名+n1,数组名+n2,值,排序函数bool cmp ) - 数组名
二分查找下界 (大于等于值) 返回值是一个指针 可以减去 数组名+n1 得到下标。找不到即 数组名+n2。upper_bound(数组名+n1,数组名+n2,值,排序函数bool cmp) - 数组名
二分查找上界 即 大于值
5.全排类函数
-
next_permutation()
-
如果没有下一个排列组合,返回false,否则返回true。每执行next_permutation()一次,会把新的排列放到原来的空间里。
-
先用sort()排序,得到最小排列后,然后再执行next_permutation()。
#include <bits/stdc++.h>
using namespace std;
int main(){
string s="bca";
sort(s.begin(),s.end()); //字符串内部排序,得到最小的排列“abc”
do{
cout<<s<<endl;
}while(next_permutation(s.begin(),s.end()));
return 0;
}
-
如果序列中有重复元素,next_permutation()生成的排列会去重。例如“aab”,代码输出3个排列{aab, aba, baa},不是6个排列。
-
自写全排列函数 --深度优先搜索
#include<bits/stdc++.h>
using namespace std;
int a[20] = {1,2,3,4,5,6,7,8,9,10,11,12,13};
bool vis[20]; //记录第i个数是否用过
int b[20]; //生成的一个全排列
void dfs(int s,int t){
if(s == t) { //递归结束,产生一个全排 列
//if(s == 3) { //递归结束,取3个数产生一个排列
for(int i = 0; i < t; ++i) //i<3
cout << b[i] << " "; //输出一个排列
cout<<endl;
return;
}
for(int i=0;i<t;i++)
if(!vis[i]){
vis[i] = true;
b[s] = a[i];
dfs(s+1,t);
vis[i] = false;
}
}
int main(){
int n = 3;
dfs(0,n); //前n个数的全排列
return 0;
}
6.unipue函数
include<algorithm>
- sort先排序 unipue后去重
- 返回不重复个数 len=unipue(s,s+12) - s ;
7.数组vector
- 加入
v.push_back
删除v.popback()
清空v.clear
n=v.size()
- 可以直接 用下标访问
- 迭代器访问
for( auto i:v ) cout<<i;
8.集合set
- 自动排序+去重
- 迭代器遍历
- 函数:
9.映射map
multiset的排序容器
#include<bits/stdc++.h>
#include<set>//使用multiset set 需要使用此头文件
using namespace std;
int main (){
multiset<int> st;
int a[10]={ 1,14,12,13,7,13,21,19,8,8};
for(int i=0;i<10;i++){
st.inset(a[i])//插入的是a[i]的复制品
}
multiset<int>::iterator i;//迭代器 类似于指针
for( i = st.begin();i!=st.end();++i){
cout<<*i<<",";
}
cout<<endl;
//输出1 7 8 8 12 13 13 14 19 21
}
10.链表list
11.栈和队列
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qEWdrAyH-1663738769822)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220731095946294.png)]
12 查找find
2.动态规划
【带备忘录的递归】
最长公共子序列
int dp[maxn][maxn];
void init(int n, int m) {//初始化
for(int i = 0; i < n; i++) {
dp[i][0] = 0;
}
for(int i = 0; i < m; i++) {
dp[0][i] = 0;
}
}
void LCS(string s1, string s2) {
int n = s1.length(), m = s2.length();//两个字符串从下标为1开始存
init(n, m);
for(int i = 1; i < n; i++) {//每一位遍历
for(int j = 1; j < m; j++) {
if(s1[i] == s2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
}
最长上升子序列
dp[i] 表示长度为 i 的 LIS 的结尾元素
int len;//LIS的长度
int s[maxn];//存储原始序列
int dp[maxn];
void init() {//初始化
//序列第一位直接放进去就行
len = 1;
dp[len] = s[1];
}
void LIS() {//从下标为1开始存储数据
init();
for(int i = 2; i <= n; i++) {//n为所给序列长度
if(s[i] > dp[len]) {//如果比当前LIS最后一位要大就直接接上去
dp[++len] = s[i];
}
else {//否则就修改数据
int pos = lower_bound(dp + 1, dp + len + 1, s[i]) - dp;
dp[pos] = s[i];
}
}
}
最大递增子序列和
#include<cstdio>
#include<cstring>
#include<map>
#include<algorithm>
using namespace std;
#define INF 0x3f3f3f3f
//max,min,swap,unique
//dp[i]:以i结尾的数字可构成的最大和
// dp[i] = max(num[i] , dp[j:(1~i-1)] + num[i]) num[i] > num[j]
int num[1000+5];
int dp[1005]; //表示以 第i个数字 结尾的子串的最大和
int main()
{
int n;
while (~scanf ("%d",&n) && n)
{
for (int i = 1 ; i <= n ; i++)
scanf ("%d",&num[i]);
int ans = -INF;
for (int i = 1 ; i <= n ; i++)
{
dp[i] = num[i];
for (int j = 1 ; j < i ; j++)
{
if ( num[j]<num[i] )
dp[i] = max(dp[i] , dp[j] + num[i]);
ans = max(ans , dp[i]);
}
}
printf ("%d\n",ans);
}
return 0;
}
3.快速幂
1.a^b%c 如何求?
#include<iostream>
using namespace std;
long long ksm(long long a,long long b,long long c){
long long ans=1;
while(b>0){
if(b&1){//相当于b%2==1
ans = ans * a %c;
}
a=a * a %c;
b>>=1;// 相当于b=b/2;
}
return ans;
}
int main(){
int a=3,b=3,c=25;
cout<<ksm(a,b,c);
}
4.记忆化搜索
以斐波那契数列为例
int F(int n){
if(n<2)f[n]=1; //这里f[]是储存数据的数组
else if(f[n]==0) //这里是重点
f[n]=F(n-1)+F(n-2);
return f[n];
}
注释:当f[n]没被计算过,就计算一次。也就是说,如果f[n]已经被计算过储存起来了,那就直接用储存的数据,便不用再计算了。
5.高精度
1.加法
方法:按位加再进位
#include<bits/stdc++.h>
using namespace std;
string add(string sa,string sb)
{
int lena=sa.size(),lenb=sb.size(),ma=max(lena,lenb);
int a[1005]={0},b[1005]={0},c[1005]={0};
for(int i=0;i<lena;i++) a[i]=sa[lena-i-1]-'0';
for(int i=0;i<lenb;i++) b[i]=sb[lenb-i-1]-'0';
for(int i=0;i<ma;i++){
c[i]+=a[i]+b[i];
if(c[i]>9) c[i+1]=c[i]/10,c[i]%=10;
}
if(c[ma]!=0) ma++;
string s="";
for(int i=ma-1;i>=0;i--) s+=char(c[i]+'0');
return s;
}
int main()
{
string a,b;
while(cin>>a>>b){
cout<<add(a,b)<<endl;
}
return 0;
}
2.减法
#include <iostream>
#include <string>
#include <cstring>
using namespace std;
int main(){
//计算 a - b = ans
int i,j,flag=0; //flag用来记录是否为负数, 1代表负数, 默认为0即正数
string a_s,b_s; //用字符串接受两个数字
int a[521]={0},b[521]={0},ans[521]={0}; //三个存数字数组, 记得初始化为0
cin>>a_s>>b_s; //接受输入的字符串型 a 和 b 存到 a_s 和 b_s 里
int len_a=a_s.length(),len_b=b_s.length(); //计算 a 和 b 的长度
int len=len_a>len_b?len_a:len_b; //计算 a 和 b 的最大长度, 确定做减法时的循环次数
for(i=0;i<len_a;i++) a[i]=a_s[len_a-1-i]-'0'; //将字符型数字转化为整形数字
for(i=0;i<len_b;i++) b[i]=b_s[len_b-1-i]-'0'; //注意要倒序存储, 后面输出的时候再倒一遍
if(len_a>len_b||len_a==len_b&&a[len_a-1]>=b[len_b-1]){ //当 a 大于 b 时
for(i=0;i<len;i++){
if(a[i]<b[i]){ //当对应位 a < b 时, 向高位借位
a[i+1]--; //高位减1
a[i]+=10; //这一位加10
}
ans[i]=a[i]-b[i]; //做减法
}
}
else{ //当 a 小于 b 时, a - b 为负数, 所以变为 b - a, 保证减法结果为正
flag=1; //负数标记
for(i=0;i<len;i++){
if(b[i]<a[i]){ //借位
b[i+1]--;
b[i]+=10;
}
ans[i]=b[i]-a[i];
}
}
if(flag) cout<<"-"; //输出负号
while(ans[len]==0&&len>0) len--; //去掉前置无用的 0
for(i=len;i>=0;i--){ //倒序输出结果
cout<<ans[i];
}
}
3.乘法
#include<bits/stdc++.h>
using namespace std;
string mul(string sa,string sb)
{
int lena=sa.size(),lenb=sb.size(),lanab=lena+lenb;
int a[1005]={0},b[1005]={0},c[1005]={0};
for(int i=0;i<lena;i++) a[i]=sa[lena-i-1]-'0';
for(int i=0;i<lenb;i++) b[i]=sb[lenb-i-1]-'0';
for(int i = 0; i < lena; i++){
for(int j = 0; j < lenb; j++){
c[i + j] += a[i] * b[j];//按位相乘并累加
}
}
int x = 0;
for(int i = 0; i < lanab; i++){//处理进位
c[i] += x;
x = c[i]/10;
c[i] %= 10;
}
int k = lanab,flag=0;//两数相乘后的位数不会超过两数位数相加之和,所以c1足够了
while(c[k]==0 && k > 0){//处理前缀0
k--;
flag++;
}
int ma=lanab-flag+1;
string s="";
for(int i=ma-1;i>=0;i--) s+=char(c[i]+'0');
return s;
}
int main()
{
string a,b;
while(cin>>a>>b){
cout<<mul(a,b)<<endl;
}
return 0;
}
4.除法
1.高精除低精
#include<iostream>
#include<cstdio>
#include<string>
#include<string.h>
#include<cmath>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f
#define maxn 5e5+5
//高精度除以低精度
char s1[500005];//高精度
ll b; //低精度
int a[500005];
int ans[500005];
void solve(){
memset(a,0,sizeof(a));
memset(ans,0,sizeof(ans));
//scanf("%s %lld",s1,&b);
int n=strlen(s1);
for(int i=0;i<n;i++) a[i]=s1[i]-'0';
ll pos=0;//存储余数
for(int i=0;i<n;i++){
ans[i]=(pos*10+a[i])/b;
pos=(pos*10+a[i])%b;
}
int len=0;// 记录前导零长度
while(ans[len]==0&&len<n-1) ++len;
//for(int i=len;i<n;++i) printf("%d",c[i]);
printf("%lld\n",pos);
}
int main()
{
while(~scanf("%s %lld",s1,&b))
solve();
return 0;
}
2.高精除高精
#include<iostream>
#include<cstdio>
#include<string>
#include<string.h>
#include<cmath>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f
#define maxn 5e5+5
//高精度除以高精度
string s1,s2;//这里用string存,会更加方便
int a[500005];
int b[500005];
int c[500005];
int tmp[500005];
void sub(string x,string y) //传入相减的字符串长度
{ memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
memset(tmp,0,sizeof(tmp));
int n = x.size();
int m = y.size();
for(int i=0;i<n;++i) a[n-i-1] = x[i] - '0';
for(int i=0;i<m;++i) b[m-i-1] = y[i] - '0';
int len = max(n,m);
for(int i=0;i<len;++i) {
if(a[i]<b[i])
{
a[i+1] -= 1; //向上借位
a[i] += 10; //向上接了一位,故+10
}
tmp[i] = a[i] - b[i];
}
while(tmp[len]==0&&len!=0)
--len;
//这里注意要更新s1的值
string ans = "";
for(int i=len;i>=0;--i) ans += tmp[i] + '0';
s1 = ans;
}
int cmp(string s1,string s2)
{ /*
s1>s2 返回1
s1==s2 返回0
s1<s2 返回-1
*/
int n = s1.size();
int m = s2.size();
if(n<m) return -1;
else if(n==m&&s1==s2) return 0;
else if(n==m&&s1<s2) return -1;
return 1;
}
void div()
{ cin>>s1>>s2;
memset(c,0,sizeof(c));
int n = s1.size();
int m = s2.size();
int len = n - m; //商的位数
if(len<0) //特别处理
{
printf("0\n");
return;
}
for(int i=len;i>=0;--i)
{
memset(b,0,sizeof(b));
int pos = i; //需要添加0的个数
string ans = s2; //临时字符串变量
while(pos--) ans += '0';
while(cmp(s1,ans)>=0) {
++c[i];
sub(s1,ans); //高精度除法
}
}
while(c[len]==0&&len!=0) --len;
for(int i=len;i>=0;--i) printf("%d",c[i]);
}
int main(){
div();
return 0;
}
6.手动二分
//给出单调递增的整数序列 ,查找第一个大于或等于 的数的位置。
while(l<r){
int mid=(l+r)>>1;//l表示区间左端点,r表示区间右端点
if(a[mid]>=x)//x为目标值
r=mid;
else l=mid+1;
}
//区间[l,r)
//另一种写法
while(l<r){
int mid=(l+r+1)>>1;//l表示区间左端点,r表示区间右端点
if(a[mid]>=x) //x为目标值
r=mid-1;
else l=mid;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q2zL0JFc-1663738769823)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220302130717123.png)]
//上下为 左右之差别
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rc4mB4lb-1663738769823)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220302130742401.png)]
7.尺取
//给定一个正数序列,求满足和大于或等于S的子序列(连续)的最短长度
while(1){
while(r<n&&sum+a[r]<s){//
sum+=a[r];
r++;
}
if(r==n)break;
ans=min(ans,r-l+1);//
sum-=a[l]; //
l++;
}
//第二种写法
int l=r=1,sum=0.ans=100001;
while(1){
sum+=a[r++];//右端点右移
if(sum>=s){
while(sum>=s) sum-=a[l++];//左端点右移
ans=min(ans,r-l+1);
}
if(r==n+1) break;//到头了
}
8.并查集
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集
合(根节点作为集合的代表元素),我们只要找到了某个元素的的树根,就能确定它在
哪个集合里。
多少个根节点 就有多少个集合
- 注意:还可以让花费最小的当祖先
const int MAXN = 1005;
int fa[MAXN];
void init() {//初始化
for (int i=1; i<=MAXN; i++) {//1开始 不过这无关紧要
fa[i]=i;
}
}
int find(int x) { //找x的祖先
if (fa[x]==x)
return x;
else
fa[x]=find(fa[x]);
return fa[x]; // 在查询根节点的过程中,进行路径压缩
}
void union(int u,int v) { //u的祖先是v
int u_fa = find(u);//找到u的祖先
int v_fa = find(v);//找到v的祖先
fa[u_fa] = v_fa; //uv的祖先合并
}
按秩合并
void merge(inr i,inr j){
int x=find(i);
int y=find(j);
if(rank[x]<=rank[y]) fa[x]=y;
else fa[y]=x;
if(rank[x]==rank[y]&&x!=y) rank[y]++;
}
9.倍增RMQ(区间最值查询)
rmp [ i ] [ j ]表示i开始 2的 j 次方的最小值
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<string.h>
#include<cmath>
#include<vector>
#include<stack>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
#define INF 0x3f3f3f3f
int n,q;
int rmqmin[50005][16];
int rmqmax[50005][16];
int lo[50005];
//初始化
void rmqinit(){
for (int j = 1; (1<<j) <= n; ++j)
for (int i = 1; i + (1 << j) - 1 <= n; ++i){
rmqmax[i][j] = max(rmqmax[i][j - 1], rmqmax[i + (1 << j - 1)][j - 1]);
rmqmin[i][j] = min(rmqmin[i][j - 1], rmqmin[i + (1 << j - 1)][j - 1]);
}
}
//调用
int rmi(int l,int r){
//int k=log2(l-r+1)
int k=lo[r-l+1];
return min(rmqmin[l][k],rmqmin[r-(1<<k)+1][k]);
}
int rma(int l,int r){
//int k=log2(l-r+1)
int k=lo[r-l+1];
return max(rmqmax[l][k],rmqmax[r-(1<<k)+1][k]);
}
int main(){
cin>>n>>q;
//构造
for(int i=1;i<=n;i++){
cin>>rmqmax[i][0];
rmqmin[i][0]=rmqmax[i][0];
}
lo[1]=0;
for(int i=2;i<=n;i++){
lo[i]=lo[i/2]+1;//打表log2的值
}
rmqinit();
while(q--){
int l,r;
cin>>l>>r;
cout<< rma(l,r)-rmi(l,r) <<endl;
}
return 0;
}
10.图和树的存储和遍历
图
链式前向星存图
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-doE4tXK3-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220706192431064.png)]
- 链式前向星高效存图:head数组下标是节点,数组存的其实 是“第几条边”。
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nl0SQlQB-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220805065338218.png)]
-
编号i 1 2 3 4 5 6 起点u C A B A C C 终点v A C C B B D 边权w 4 1 6 4 1 2 下一条边的编号next -1 -1 -1 2 1 5
#include<iostream>
#include<cstring>
using namespace std;
struct E{
int to;
int w;
int next;
}edge[100];
int head[100],tot;//图 head是什么?
void init() { //初始化图
tot=1;
memset(head,-1,sizeof(head));
}
void addedge(int a,int b,int w)
{ //添加一条a到b边权为w的有向边,无向边就正反各加一次
edge[tot].to=b;
edge[tot].w=w;
edge[tot].next=head[a];
head[a]=tot++; //增加一个边
}
//遍历点x的连的出边
int main(){
init();
addedge(1,2,3);
addedge(1,4,4);
addedge(1,3,1);
addedge(2,5,2);
addedge(4,5,5);
addedge(5,3,6);
// addedge(5,1,7);
cout<<"tot="<<tot<<endl;
for(int i=1;i<=tot;i++){
cout<<head[i]<<endl;
}
int x=1;
for(int i=head[x];i!=-1;i=edge[i].next) {
int to=edge[i].to;
int w=edge[i].w;
printf("点x到点%d,边权为%d的边\n",to,w);
}
return 0;
}
邻接矩阵 存图
-
空间复杂度高
-
[n]--[next]
->to w next
->to w next
-
//代码1 int tu[maxn][maxn];//图 memset(tu,0,sizeof(tu));//初始化图 tu[a][b]=w;//添加一条a到b的边权为w的有向边 tu[a][b]=tu[a][b]=w;//添加一条a到b的边权为w的无向边 //遍历点x连的边 for(int i=1;i<=n;i++) if(i!=x&&tu[x][i]!=0) { printf("点x到点%d,边权为%d的边\n",i,tu[x][i]); } //代码2 int dis[n][n]; int inf=99999999; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(i!=j)dis[i][j]=inf; cin>>u>>v>>w; dis[u][v]=w;
邻接表 存图
vector<int>v[maxn],g[maxn];//图
for(int i=0;i<maxn;i++) v[i].clear(),g[i].clear();//初始化图
v[a].push_back(b);//添加一条a到b的边权为w的有向边
g[a].push_back(w);
v[a].push_back(b); //添加一条a到b的边权为w的无向边
g[a].push_back(w);
v[b].push_back(a);
g[b].push_back(w);
//遍历点x的连的出边
for(int i=0;i<v[x].size();i++) {
int to=v[x][i],w=g[x][i];
printf("点x到点%d,边权为%d的边\n",to,w);
}
采用vector
veotor<int>g[maxn];
struct node{
int v;
int w;
node(int y,int z){
v=y;
w=z;
}
};
加边
int x,y,z;
cin>>x>>y>>z;
g[c].push_back(node(y,z));
遍历
for(int i=0;i<g[u.id].size();i++){
node y=g[u.id][i];
}
树
无环、任意两个点互通 (没有节点叫做空树,空树也是树)
1.孩子数组 (动态开点)
struct node{
vector<int> childern,value;
}tree[N];
void add(int nod,int child,int w){
tree[nod].childern.push_back(child);
tree[nod].value.push_back(w);
}
void dfs(int now,int fa){
for(int i=0;i<tree[now].child.size();i++){
int next=tree[now].child[i];
if(next!=fa){
cout<<next<<" "<<tree[now].value[i];
dfs(next,now);
}
}
}
2.孩子数组 (固定开点)
#define lson (o<<1)
#define rson (o<<1|1)
#define father (o>>1)
int value[maxn];
int main(){
int x=1;
value[x]=5;
value[lson]=6;
value[rson]=7;
cout<<father(lson(x))<<endl;
return 0;
}
1.邻接矩阵存树
#define lson (o<<1)
#define rson (o<<1|1)
#define father (o>>1)
vector<int> v[maxn],g[maxn];
2.邻接表存树
3.链式前向星
使用一个数组存每一个结点的最后一条边,然后通过最后一条边找和这个点相连的其他边
4.树的遍历
- 前、中、后 序
11.排序
1.冒泡
o(n^2)
for(int i = 0;i < n-1 ;i++){
for(int j = 0 ;j < n-i-1 ; j++){
}
}
2.桶排序
(空间换时间)(0数据范围+m)
适合整数 且数据范围小的时候
# define Max 100005
int main(){
int n;
scanf("%d",&n);
int a[Max];
memset(a,0,sizeof(a));
int date;
for(int i=0;i<n;i++){//读入数据并装入桶中
cin>>date;
a[date]++;
}
for(int i=0;i<=n;i++){//按照桶来输出
while(a[i]>0){
cout<<i;
a[i]--;
}
}
printf("\n");
return 0;
}
3.快速排序
//快速排序 函数
sort(a,a+n,cmp);
bool cmp(xuesheng a,xuesheng b){
if(a.sum ==b.sum ) return a.k <b.k ;
else return a.sum >b.sum ;
}
//底层模板
void QuickSort(ElementType A[],int low,int high){
if(low<high){
int pivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
int Partition(ElementType A[],int low,int high){
ElementType pivot=A[low];
while(low<high){
while(low<high&&A[high]>=pivot)high--;
A[low]=A[high];//比枢轴小的移动到左边
while(low<high&&A[low]<=pivot)low++;
A[high]=A[low];//比枢轴大的移动到右边
}
A[low]=povot;
return low;//返回放枢轴的最终位置
}
4.归并排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylY4Rzl6-1663738769824)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220306093152199.png)]
void merge(ElementType A[],int low,int mid,int high){
for(int k=low;k<=high;k++){
B[k]=A[k];//放在B中临时存放
}
for(int i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
if(B[i]<=B[j]){
A[k]=B[i++];
}else A[k]=B[j++];
}
while(i<=mid)A[k++]=B[i++];
while(j<=high)A[k++]=B[j++];
}
void MergeSort(ElementType A[],int low,int high){
if(low<high){
int mid=(low+high)/2;
MergeSort(A,low,mid);
MergeSort(A,mid+1,high);
Merge(A,low,mid,high);
}
}
12.递推递归
1.记忆化搜索-fib
搞一个数组把计算过值存起来
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p2vzau9O-1663738769825)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220313090117956.png)]
13.简单数学与简约枚举
1.组合数
-
计算方法:C(n,m)=n!/(m!*(n-m)!)
-
(性质1) C(n,k)=C(n,n-k)。(对称性)
-
(性质2) C(n,k-1)+C(n,k)=C(n+1,k)。(递推公式)—左手三角形
-
(性质3) ΣC(n,i)=C(n,0)+C(n,1)+…+C(n,n)=2^n。(横向求和)
-
打表 n<30 int n<64 long long
-
C(n,0)=1,C(n,k+1)=C(n,k)*(n-k)/(k+1)。
long long c(int n , int m){
long long ans =1;
for (int i=o;i<m;i++){
ans=ans*(n-1)/(n+1);
}
return ans ;
}
-
已知正整数N,可以将它拆分为若干正整数之和,问共有多少种不同的拆法?---------------挡板法
-
在1到N的范围内,有多少个正整数可以被2、3、5中的任意一个整除 ?------------容斥原理 能被2整除的数有N/2个,能被3整除的数有N/3个,能被5整除的数有N/5个
-
错排问题----D(n)=(n−1)[D(n−1)+D(n−2)]
2.枚举子集
1.增量构造法
int arr[10],pos[10];
//arr[]集合的数组
//pos[]选中子集下标的数组
//n 集合的元素个数
//cur 初始0 表示子集的个数
void print_subset0(int n,int*pos,int cur){
//输出目前子集
for(int i=0;i<cur;i++){
printf("%d ",arr[pos[i]]);
}
printf("\n");
//选择最小的未被选择的集合的一个元素
int st=cur?pos[cur-1]+1:0;
for(int i=st;i<n;i++){
pos[cur]=i;
print_subset0(n,pos,cur+1);
}
}
14.位运算
按位与 | a&b | 两者同时为1则为1,否则为0 | 00101&11100=00100 |
---|---|---|---|
按位或 | a l b | 有1为1,无1为0 | 00101 l 11100=11101 |
按位异或 | a^b | 相同为0,不同为1 | 00101^11100=11001 |
按位取反 | ~a | 是1为0,是0为1 | ~00101=11010 |
左移 | a<<b | 把对应的二进制数左移b位 | 00101<<2=0010100 |
右移 | a>>b | 把对应的二进制数右移b位 | 00101>>1=0010 |
一、取出x的第i位:y = ( x>> ( i-1 ) ) & 1
二、将x的第i位取反:x = x ^ ( 1<< ( i-1 ) )
三、将x的第i位变为1:x = x | ( 1<< ( i-1 ) )
四、将x的第i位变成0:x = x & ~( 1<< ( i-1 ) )
五、将x最靠右的1变成0:x = x & (x-1)
六、取出最靠右的1:y=x&(-x)
七、把最靠右的0变成1: x | = (x-1)
-
n&(n-1)
:将n的二进制表示中最低位1改为0;例题:求数组中出现次数超过一半的元素?- 快速排序,排序后输出中间的那个数字。
- 位运算统计每一位
-
(a*b)%m==(a%m ×b%m)% m
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Z459MT3-1663738769825)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220410075103631.png)]
1.快速幂模板
long long Pow(long long a,long long b,long long m){
long long ans =1;
while(b>0){
if(b&1){
ans=ans*a%m;
}
a=a*a%m;
b>>=1;
}
return ans;
}
2.快速乘模板
long long mul(long long a,long long b,long long mod){
long long ans =0;
a%=mod;
while(b>0){
ans+=a;
if(ans>mod) ans-=mod;
a<<=1;
if(a>=mod) a-=mod;
b>>=1;
}
return ans;
}
15深度优先搜索
查找环
二分图检验(dfs染色)
for(int i=0;i<n;i++){
if(col[i]==0&&flag){
//图第一个颜色
col[i]=1;
dfs(i);
}
}
void dfs(int now){
if(!flag) return;
for(int i=0;i<col[now].size();i++){\
int next=col[now][i];
if(!col[next]){
col[next]=3-col[now];
dfs(next);
}
else if(col[nest]==col[now]){
flag=0;
return;
}
}
}
16广度优先搜索
bfs搜图-队列
17.树状数组和线段树
离散化
去重
for(int i=1;i<=n;i++){
if(i==1||a[i]!=a[i-1])
c[++tot]=a[i];
}
树状数组(前缀和&单点修改)
下标从1开始
典型的区间更新,单点查询问题,用树状数组求解最方便
树状数组的查询前缀和
如果要求a序列[l,r]的区间和,只需要计算ask(r)-ask(l-1)即可。
int ask(int x){
int ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
树状数组的单点修改
要对序列a中的某个数a[x]进行修改,将其值增加y ,,对于维护的前缀和数组c我们需要进行相应的修改以确保区间和的正确性。
void insert(int x,int y){
while(x<=N){
c[x]+=y;
x+=lowbit(x);
}
}
应用–求逆序对数
#include<iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>
using namespace std;
//求逆序对数 涉及离散化
const int N=5e5+5;
int a[N],b[N],c[N],t[N],tot;
//原数组 复制数组 离散化数组 树状数组
int lowbit(int x){
return x&(-x);
}
int ask(int x){//
int ans=0;
while(x){
ans+=t[x];
x-=lowbit(x);
}
return ans;
}
void insert(int x,int y){
while(x<=tot){
t[x]+=y;
x+=lowbit(x);
}
}
int main()
{
int n;
while(scanf("%d",&n)&&n){
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
b[i]=a[i];
}
tot=0;
sort(a+1,a+1+n);
memset(t,0,sizeof t);
for(int i=1;i<=n;i++){//离散化
if(i==1||a[i]!=a[i-1])
c[++tot]=a[i];
}
long long ans=0;
for(int i=n;i>=1;i--){
int idx=lower_bound(c+1,c+1+tot,b[i])-c;
ans+=ask(idx); //查找前缀和
insert(idx,1);
}
printf("%lld\n",ans);
}
return 0;
}
线段树
下标从1开始
数组写法
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
#define ls rt<<1 //z*n
#define rs rt<<1|1 //z*n+1
const int N=5e4+5;
int a[N],t_max[N<<2],t_min[N<<2];//t数组维护区间信息
void push(int rt){
t_max[rt]=max(t_max[ls],t_max[rs]);//更新节点信息
t_min[rt]=min(t_min[ls],t_min[rs]);
//
}
void build(int rt,int l,int r){
if(l==r){
int x;
scanf("%d",&x);
t_max[rt]=x;
t_min[rt]=x;
return;
}
int mid=l+r>>1;
build(ls,l,mid);//递归建树
build(rs,mid+1,r);
push(rt);//将信息由子节点更新到父节点
}
int query_max(int rt,int l,int r,int L,int R){//[L,R]表示查询区间
if(L<=l&&r<=R){//1
return t_max[rt];
}
int mid=l+r>>1;
int ans=-0x3f3f3f3f;//负无穷
if(L<=mid) ans=max(ans,query_max(ls,l,mid,L,R));//2
if(R>=mid+1) ans=max(ans,query_max(rs,mid+1,r,L,R));//3
return ans;
}
int query_min(int rt,int l,int r,int L,int R){//[L,R]表示查询区间
if(L<=l&&r<=R){//1
return t_min[rt];
}
int mid=l+r>>1;
int ans=1e8;
if(L<=mid) ans=min(ans,query_min(ls,l,mid,L,R));//2
if(R>=mid+1) ans=min(ans,query_min(rs,mid+1,r,L,R));//3
return ans;
}
//void insert(int rt,int l,int r,int x,int v){
// if(l==r){
// t[rt]=v;
// return ;
// }
// int mid=l+r>>1;
// if(x<=mid){//修改的元节点包含在左节点
// insert(ls,l,mid,x,v);
// }else
// insert(rs,mid+1,r,x,v);
// push(rt);//更新信息
//}
int main(){
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
// for(int i=1;i<=n;i++){
// cin>>a[i];
// }
memset(t_max,0,sizeof(t_max));
memset(t_min,0,sizeof(t_min));
build(1,1,n);
while(m--){
int L,R;
scanf("%d%d",&L,&R);
int ans=query_max(1,1,n,L,R)-query_min(1,1,n,L,R);
printf("%d\n",ans);
}
}
return 0;
}
结构体写法
#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1e5+10;
int n,m;
int a[maxn];
struct node
{
int l,r;
long long v;
long long sum;
}tree[maxn*4];
void bulid(int x,int l,int r)
{
tree[x].l=l;
tree[x].r=r;
if(tree[x].l==tree[x].r)
{
tree[x].sum=a[l];
return;
}
int mid=(l+r)>>1;
bulid(x<<1,l,mid);
bulid(x<<1|1,mid+1,r);
tree[x].sum=tree[x<<1].sum +tree[x<<1|1].sum ;
}
void xp(int x)// 精髓
{
if(tree[x].v)
{
tree[x<<1].sum +=tree[x].v *(tree[x<<1].r -tree[x<<1].l +1);
tree[x<<1|1].sum +=tree[x].v *(tree[x<<1|1].r -tree[x<<1|1].l +1);
tree[x<<1].v +=tree[x].v ;
tree[x<<1|1].v +=tree[x].v ;
tree[x].v =0;
}
}
void change(int x,int l,int r,int k)
{
if(l<=tree[x].l&&tree[x].r<=r)
{
tree[x].sum +=k*(tree[x].r-tree[x].l +1);
tree[x].v +=k;
return ;
}
xp(x);
int mid=(tree[x].l +tree[x].r )>>1;
if(l<=mid)
{
change(x<<1,l,r,k);
}
if(r>mid)
{
change(x<<1|1,l,r,k);
}
tree[x].sum =tree[x<<1].sum +tree[x<<1|1].sum ;
}
long long query(int x,int l,int r)
{
if(l<=tree[x].l &&tree[x].r <=r)
{
return tree[x].sum ;
}
xp(x);
long long ans=0;
int mid=(tree[x].l+tree[x].r )>>1;
if(l<=mid)
{
ans+=query(x<<1,l,r);
}
if(r>mid)
{
ans+=query(x<<1|1,l,r);
}
return ans;
}
18.最短路径 wiki
Dijkstra
- n-1 n n
const int maxn = 105;
const int infinite = 1e9;
int edge[maxn][maxn]; //存边的权值
int vertex_num, edge_num;
int path[maxn]; //存放最短路径——>倒序存放——>若要打印原点到某点的距离,需要使用递归
int short_path_table[maxn]; //存放各点到原点最短路径的权值和
bool final[maxn]; //用于标记某点是否已求得到原点最短路径——>即已为确定名单
int v0=1;
void init()
{
memset(final, 0, sizeof(final));
memset(path, -1, sizeof(path)); //-1 用于标记此点还未加入
short_path_table[v0] = 0; //原点到原点的距离为0
final[v0] = true; //标记 v0 已求得到原点最短路径
for (int i = 1; i <= vertex_num; i++)
{ //导入初始时每个点到 v0 点的距离
short_path_table[i] = edge[v0][i];
}
}
void Dijkstra()
{
init();
//开始主循环,每次求得v0到某个v顶点的最短路径
for (int m = 1; m < vertex_num; m++)
{ //共有 vertex_num - 1 个顶点需要找到最短路径
int min = infinite;
int min_index; //池子中最小值的下标
//寻找离v0最近的顶点——>从short_path_table池子中选出最小值
for (int i = 1; i <= vertex_num; i++)
{
if (!final[i] && short_path_table[i] < min)
{ //在final名单外选最小值
min = short_path_table[i];
min_index = i;
}
}
final[min_index] = true; //将选中的最小值加入 final 名单
//修正当前点周围点的最短路径及距离
for (int i = 1; i <= vertex_num; i++)
{
// min + edge[min_index][i] < short_path_table[i]——>如果从 min_index 点到 i 点的距离比原来短时
if (!final[i] && min + edge[min_index][i] < short_path_table[i])
{
short_path_table[i] = min + edge[min_index][i];//更新到 i 点最短长度
path[i] = min_index;
}
}
}
}
int main()
{
memset(edge,0x3f,sizeof(edge));//初始化
memset(short_path_table, 0x3f, sizeof(short_path_table));
cin>>edge_num>>vertex_num;
for(int i=1;i<=edge_num;i++){
int a,b,c;
cin>>a>>b>>c;
if(edge[a][b]>c){//注意
edge[a][b]=c;
edge[b][a]=c;
}
}
Dijkstra();
cout<<short_path_table[vertex_num]<<endl;
return 0;
}
Bellman-Ford 算法 1
const int max_edge = 5500;
const int max_vertex = 600;
struct EDGE
{
int from, to, cost;
} edge[max_edge]; //存边——注意,这里只存了单向边
int dis[max_vertex]; //动态存储到_起点_的距离
int vertex_num, edge_num;
//求解从起点出发的最短距离,同时检查是否有“负圈”(注意:不同于“负边”)
// st 为起点
//若存在负圈则返回 false
bool shortest_path(int st)
{
memset(dis, 0x3f, sizeof(dis)); //将初始值设为最大
//***************************************************************
//当将 dis 数组初始为 0 时负环之间将不会互相影响,于是可以独立找出所有的负环——>借助并查集
//***************************************************************
dis[st] = 0; //起点距离起点为 0
for (int i = 0; i < vertex_num; i++) //更新一次代表加入一个点
{ //除去起点 v0,所以最多更新 vertex_num - 1 次
for (int j = 0; j < edge_num; j++) //遍历所有的边
{
EDGE &e = edge[j]; //对 edge[j] 取一个别名
if (dis[e.from] < 0x3f3f3f3f && dis[e.to] > dis[e.from] + e.cost)
// dis[e.from] < 0x3f3f3f3f:避免使用未更新的点,发生溢出之类的异常
{ //若有优化——>相当于更新了一个点
dis[e.to] = dis[e.from] + e.cost;
if (i == vertex_num - 1)
//由于是从 0 开始,所以 vertex_num - 1 是第 vertex_num 次
return false;
}
}
}
return true; //检查完毕,所有的最短路径都在 vertex_num - 1 次中找到——>不存在负环
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int path, worm;
scanf("%d%d%d", &vertex_num, &path, &worm);
edge_num = 2 * path + worm;
for (int i = 0; i < 2 * path; i++)
{
scanf("%d%d%d", &edge[i].from, &edge[i].to, &edge[i].cost); //允许输入“重边”
i++;
edge[i].from = edge[i - 1].to;
edge[i].to = edge[i - 1].from;
edge[i].cost = edge[i - 1].cost;
}
for (int i = 2 * path; i < 2 * path + worm; i++)
{
scanf("%d%d%d", &edge[i].from, &edge[i].to, &edge[i].cost); //允许输入“重边”
edge[i].cost = -edge[i].cost;
}
if (shortest_path(1)) //起点为 1
{
printf("NO\n");
}
else
{
printf("YES\n");
}
}
}
Floyd 算法
const int max_v = 1e4;
const int INF = 1e6; //注意 `溢出` 问题
int vertex_num;
int dis[max_v][max_v]; //邻接矩阵存图:从 i 到 j 的距离(不存在则为无穷大)
int path[max_v][max_v]; //从 i 到 j 最短路径的中第一步应该走的节点为 path[i][j]
void Floyd()
{
//初始化记录路径的二维数组 path
for (int st = 0; st < vertex_num; st++)
{
for (int ed = 0; ed < vertex_num; ed++)
{
path[st][ed] = ed; //从 st 到 ed 的第一步(当然也是最后一步)是 ed
}
}
for (int transfer = 0; transfer < vertex_num; transfer++) //中转站0~transfer
for (int st = 0; st < vertex_num; st++) // st为起点
for (int ed = 0; ed < vertex_num; ed++) // ed为终点
{
// //避免溢出的写法
// if (dis[st][transfer] < INF && dis[transfer][ed] < INF)
// {
// //松弛操作
// dis[st][ed] = min(dis[st][ed], dis[st][transfer] + dis[transfer][ed]);
// }
//没有避免溢出的写法(可以通过将 INF 设置为一个比最短路最大值稍大的值来避免)
if (dis[st][ed] > dis[st][transfer] + dis[transfer][ed])
{
//松弛操作
dis[st][ed] = dis[st][transfer] + dis[transfer][ed];
//记录路径
path[st][ed] = path[st][transfer]; //先跟着到节点 transfer
}
}
}
//打印从 st 到 ed 的最短路径途径的节点
void print_path(int st, int ed)
{
//从 st 出发
printf("%d ", st);
int current = path[st][ed]; //当前走到的节点
while (current != ed)
{
printf("%d ", current);
current = path[current][ed]; //下一步走的节点
}
printf("%d\n", ed); //最后到达 ed
}
19拓扑排序和强连通分量
拓扑排序
- 有向无环图才能拓扑排序
- 英⽂名叫 Directed Acyclic Graph,缩写是 DAG。
- 入度为0 则入队。取出队首加入答案,入度减减
邻接表
vector<int>g[maxn];
priority_queue<int,vector<int>,greater<int > >q;
for(int i=1;i<=n;i++){//入度为0 ru
if(in[i]==0){
q.push(i);
}
}
int ct=0;
while(!q.empty()){
int f=q.front();
q.pop() ;
ans[++ct]=f;
pos[f]=ct;
for(int i=0;i<g[f].size() ;i++){ //删点去边
int v=g[f][i];
in[v]--;
if(in[v]==0){
q.push(v);
}
}
}
if(ct<n) //说明有环
链式前向行版本
struct node{
int to,next;
}e[maxn<<1];
void init(){
vec.clear();
cnt=tot=0;
for(int i=1;i<=n;i++){
pos[i]=deg[i]=head[i]=0;
}
}
void add(int u,int v){
e[++cnt].to=v;
e[cnt].next=head[u];
head[u]=cnt;
}
//主函数部分
while(!que.empty()){
int x=que.front();
que.pop();
pos[x]=++tot;
for(int i=head[x];i;i=e[i].next){
deg[e[i].to]--;
if(deg[e[i].to]==0){
que.push(e[i].to);
}
}
}
if(tot!=n){//有环
printf("NO\n");
}else{
强连通分量
- dfn[u] dfs到达n的次序号(时间戳)
- low[u] 从u出发的dfs树中最小的次序号
- dfs深搜(入栈、编号。遍历孩子,孩子没搜则搜则比较,搜则比较。入栈之后,dfn[x]==low[x]找到连通分量 出栈至t ==x
const int maxn=10005;
int num=0;
stack<int> s;
vector<int>g[maxn];
int num_lis=0;
int ins[maxn],dfn[maxn],low[maxn],tag[maxn];
void tarjan(int x){
s.push(x);
ins[x]=1;
dfn[x]=low[x]=++num;
for(int i=0;i<g[x].size() ;i++){
int v=g[x][i];
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
}
else if(ins[v]) low[x]=min(low[x],dfn[v]);
}
if(dfn[x]==low[x]){
int t;num_lis++;
do{
t=s.top();
ins[t]=0;
tag[t]=num_lis;
s.pop();
}while(t!=x);
}
return ;
}
20最小生成树
Kruskal 算法
- 处理边,排序后,利用并查集合并两个点
struct nod{
int u,v,w;
}e[200005];//这里不是前向行的存边方法,只是正长的存边
bool cmp(nod a ,nod b){
return a.w<b.w;
}
int fa[maxn];
int find(int x){
if(fa[x]==x)return x;
else return fa[x]=find(fa[x]);
}
bool merge(int x,int y){
int p = find(x);
int q = find(y);
if(p!=q){
fa[p] = q;
return true;
}
return false ;
}
//主函数部分
sort(e+1,e+m+1,cmp);//贪心的排序
long long ans=0,cnt=0;
for(int i=0;i<m;i++){//n-1跳出
if(cnt==n-1)break;
else if(merge(e[i].u,e[i].v)){
cnt++;
ans+=e[i].w;
}
}
for(int i=1;i<=m;i++){//n-1不跳出
if(merge(e[i].u,e[i].v)){
if(cnt<n-1) ans+=e[i].w;
cnt++;
}
}
Prime算法
- 处理点(两个集合搭桥)
// prim 算法求最小生成树
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn = 505;
int a[maxn][maxn];
int vis[maxn],dist[maxn];
int n,m;
int u,v,w;
long long sum = 0;
int prim(int pos) {
dist[pos] = 0;
// 一共有 n 个点,就需要 遍历 n 次,每次寻找一个权值最小的点,记录其下标
for(int i = 1; i <= n; i ++) {
int cur = -1;
for(int j = 1; j <= n; j ++) {
if(!vis[j] && (cur == -1 || dist[j] < dist[cur])) {
cur = j;
}
}
// 这里需要提前终止
if(dist[cur] >= INF) return INF;
sum += dist[cur];
vis[cur] = 1;
for(int k = 1; k <= n; k ++) {
// 只更新还没有找到的最小权值
if(!vis[k]) dist[k] = min(dist[k],a[cur][k]);
}
}
return sum;
}
int main(void) {
scanf("%d%d",&n,&m);
memset(a,0x3f,sizeof(a));
memset(dist,0x3f,sizeof(dist));
for(int i = 1; i <= m; i ++) {
scanf("%d%d%d",&u,&v,&w);
a[u][v] = min(a[u][v],w);
a[v][u] = min(a[v][u],w);
}
int value = prim(1);
if(value >= INF) puts("impossible");
else printf("%lld\n",sum);
return 0;
}
struct Node{
int pos,val;
bool operator < (const Node & a) const{
return a.val<val;
}
};
priority_queue<Node> q;
int dis[105];
void Prime(int s){
Node x,y;
dis[s]=0;
x.val=dis[s];x.pos=s;
q.push(x);
do{
x=q.top();q.pop();
int u=x.pos;
sum+=dis[u];
dis[u]=0;
for(int i=1;i<=n;i++){
if(Map[u][i]+dis[u]<dis[i]){
dis[i]=Map[u][i]+dis[u];
y.val=dis[i];
y.pos=i;
q.push(y);
}
}
}while(!q.empty());
}
21二分图、二分图最大匹配
dfs染色
二分图:无向图、不含奇环图
//邻接表
vector<int>g[maxn];
bool dfs(int now,int col)
{
color[now] = col;
for(int i = 0;i<g[now].size();i++){
int to = g[now][i];
if(color[now] == color[to])return false;
if(!color[to]){
if(!dfs(to,3-col))return false;
}
}
return true;
}
//链式前向星
bool dfs(int now,int col)
{
color[now] = col;
for(int i = head[now];~i;i = edge[i].next){
int to = edge[i].to;
if(color[now] == color[to])return false;
if(!color[to]){
if(!dfs(to,3-col))return false;
}
}
return true;
}
匈牙利算法
- 交替路:交错路,从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。
- 增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。
- 二分图中的一组匹配S是最大匹配,当且仅当图中不存在S增广路
- 如何找增广路? 匈牙利算法依次尝试给每一个左部节点x寻找一个匹配的右部节点y。右部点y能于左部点x匹配需要满足如下两个条件:y本身就是非匹配点,此时(x,y)就是非匹配边,自己构成长度为$ 1 $的增广路。y已经和左部一节点x′匹配,但是从x′出发能找到另一个右部点y′与之匹配。
- 本质桑使用的还是dfs递归的从x出发寻找增广路,回溯时将路径上的匹配标记取反。
int vis[maxn];//当前有无找过
vector<int>g[maxn];
int match[maxn];//match【u】=v 成功
bool dfs(int x){//当前结点x
for(int i =0;i<g[x].size();i++){//相邻
int v = g[x][i];
if(!vis[v]){
vis[v] = 1;
if(!match[v]||dfs(match[v])){//没匹配 或者匹配的点有别人可以匹配
match[v] = x;
return true;
}
}
}
return false ;
}
//主函数内
int ans = 0;
memset(match,0,sizeof match);
for(int i = 1;i <= n;i++){
memset(vis,0,sizeof(vis));
if(dfs(i)){
ans++;
}
}
22[LCA ](LCA 最近公共祖先讲义.md)
朴素算法
向上标记法
基于二分搜索的算法(倍增)
//牛
int fa[maxn][31], dep[maxn];
void dfs(int u, int faa){
fa[u][0] = faa, dep[u] = dep[faa] + 1;
for(int i = 1; i <= 30; i++){//初始化
fa[u][i] = fa[ fa[u][i - 1] ][i - 1];
}
for(int i = head[u]; i ; i = edge[i].next){ //遍历孩子 实现深搜
int v = edge[i].v;
if(v == faa)continue;
dfs(v, u);
}
}
inline int lca(int x, int y){
if(dep[x] < dep[y])swap(x,y);
for(int i = 30; i >= 0; i--){ //大跳 跳至深度一致
if(dep[ fa[x][i] ] >= dep[y]) x = fa[x][i];
}
if(x == y)return x;
for(int i = 30; i >= 0; i--){ //大跳 一起跳
if(fa[x][i] != fa[y][i]){
x = fa[x][i], y = fa[y][i];
}
}
return fa[x][0];
}
//朴素
const int max_v = 1e5 + 5;
const int max_log_v = 20;
vector<int> G[max_v];
int root;
int parent[max_log_v][max_v];
int depth[max_v],in[max_v];
int vertex_num;
void dfs(int v,int p,int d ){
parent[0][v]=p;
depth[v]=d;
for(int i=0;i<G[v].size() ;i++){//注意< 不是<= //是V
int to=G[v][i];
if(to!=p){//注意 父节点
dfs(to,v,d+1);
}
}
}
void init(){
dfs(root,-1,0);
for(int k=0;k+1<max_log_v;k++){
for(int i=1;i<=vertex_num;i++){
if(parent[k][i]==-1) parent[k+1][i]=-1;
else {
parent[k+1][i]=parent[k][parent[k][i]];
}
}
}
}
int lca(int u,int v){
if(depth[u]>depth[v]){
swap(u,v);
}
//跳到相同高度
for(int k=0;k<max_log_v;k++){
if((depth[v] - depth[u]) >> k & 1){
v=parent[k][v];
}
}
//特判
if(u==v) return u;
//一起向上跳
for(int k=max_log_v-1;k>=0;k--){//注意-1
if(parent[k][u]!=parent[k][v]){
u=parent[k][u];
v=parent[k][v];
}
}
return parent[0][u];
}
int main()
{
int t;
cin>>t;
while(t--){
scanf("%d", &vertex_num);
//初始化标记数组
cla(in);
//clb(depth);
for(int i=0;i<max_v;i++){
G[i].clear() ;
}
//输入
for (int i = 1; i < vertex_num; i++)
{
int p, b;
scanf("%d%d", &p, &b);
G[p].push_back(b);
G[b].push_back(p);
in[b]++;
}
int x, y;
scanf("%d%d", &x, &y);
//找到根节点
for(int i=1;i<=vertex_num;i++){
if(in[i]==0){
root=i ;
break;
}
}
//cout<<"root "<<root<<endl;
//lca
init();
printf("%d\n", lca(x, y));
}
return 0;
}
const int max_v = 5e5 + 5; //节点数
const int max_log_v = 20; //最多向上走 2^max_log_v 次,可以稍微开大一点(如果节点数增多,此数也会变化)
// vertex 编号从 0 开始
vector<int> G[max_v]; //图的邻接表表示——>存储各节点的孩子节点
int root; //根节点编号
int parent[max_log_v][max_v]; //向上走 2^k 步所到的节点(超过根时记作-1)
int depth[max_v]; //存储节点深度
int vertex_num; //存储节点数
//前序遍历
//预处理出 parent[0][x],并标记深度——>为之后 parent 的动态规划做铺垫
void dfs(int v, int p, int d) // vertex & parent & depth
{
parent[0][v] = p; // 2^0 = 1
depth[v] = d;
for (int i = 0; i < G[v].size(); i++)
{
if (G[v][i] != p) //避免指回父节点
dfs(G[v][i], v, d + 1);
}
}
//预处理——>降低查询**多次查询**时的 LCA 的复杂度
void init()
{
dfs(root, -1, 0); //预处理出 parent[0][x] 和 depth
//注意,这里将根节点的父节点特殊设为 -1
//通过**动态规划**预处理出 parent——>先小步,再大步
for (int k = 0; k + 1 < max_log_v; k++) // k + 1 < max_log_v——>避免超出数组长度
{
for (int i = 1; i <= vertex_num; i++)
{
if (parent[k][i] == -1) //如果已经超过根节点,则更高次幂也会超过根节点
parent[k + 1][i] = -1;
else
parent[k + 1][i] = parent[k][parent[k][i]]; //倍增——>爸爸的爸爸是爷爷
}
}
}
//计算 u & v 两点的 LCA
int lca(int u, int v)
{
if (depth[u] > depth[v]) //使得 v 深度 大于等于 u
swap(u, v);
for (int k = 0; k < max_log_v; k++)
{
if ((depth[v] - depth[u]) >> k & 1) //将两点的深度差值从低到高取出,进行跳跃——>先小跳再大跳
{ //由于每一位都以二进制形式取出了,最后的值刚好够两点深度相等
v = parent[k][v];
}
}
if (u == v) //特判当 (u, v) 的公共祖先为 u 时——>此时则不需要继续再跳了
return u;
for (int k = max_log_v - 1; k >= 0; k--)
{ //二分法思想——>先大后小的进行跳跃
if (parent[k][u] != parent[k][v]) //排除了跳过头时(包括 -1 == -1)
{
u = parent[k][u];
v = parent[k][v];
}
}
return parent[0][u]; //返回向上再走一步的的节点
}
int main()
{
// freopen("in.txt", "r", stdin);
// freopen("out_mine.txt", "w", stdout);
int query_num;
scanf("%d%d%d", &vertex_num, &query_num, &root);
for (int i = 1; i < vertex_num; i++) //只有vertex-1条边
{
int p, b;
scanf("%d%d", &p, &b);
G[p].push_back(b);
G[b].push_back(p);
}
init(); // LCA 倍增法初始化
for (int i = 0; i < query_num; i++)
{
int x, y;
scanf("%d%d", &x, &y);
printf("%d\n", lca(x, y));
}
return 0;
}
Tarjan 算法
const int maxn = 5e5 + 10; //最大节点数
vector<int> G[maxn]; //存树
int fa[maxn]; //并查集的父节点
int vertex_num;
int query_num;
int root; //节点编号
int ans[maxn]; //第 i 个答案为 ans[i]
bool vis[maxn]; //我们要对树进行前序遍历 & 后序遍历
//如果在前序遍历中节点 i 已经被访问过了,则 vis[i] = true
struct QUERY
{
int another; //被询问的另一个节点
int q_id; //在第 q_id 次被问
};
vector<QUERY> query[maxn]; //第 i 个节点上被询问其和另一个节点的最近公共祖先
void init(int n) //并查集初始化(n 个)
{
for (int i = 0; i <= n; i++)
{
fa[i] = i; //指向其本身
}
}
// 找到在集合中节点 x 的代表节点
int get_representative(int x) //递归思想
{
if (fa[x] == x)
return x;
return fa[x] = get_representative(fa[x]); //在查询根节点的过程中进行路径压缩
}
//利用 Tarjan 算法对之前的问题进行离线处理
//并将第 i 个答案存在 ans[i] 中
void Tarjan(int current, int p)
{
vis[current] = true; //标记前序遍历中此节点已经被访问过了——>在后序遍历时则可以直接回答询问
for (auto to : G[current])
{
if (to != p)
{
Tarjan(to, current); //递归
fa[to] = current; //递归完毕后将子节点归并到父节点 current 中
}
}
//在后序遍历中尝试回答 query
for (auto q : query[current])
{
//当前查询的为 [current, q.another] 的 `最近公共祖先`
if (vis[q.another]) //如果另外一个节点已经在前序遍历中访问过了,则可以回答此询问
{
ans[q.q_id] = get_representative(q.another); //存储答案到 ans 中
}
}
}
int main()
{
// freopen("in.txt", "r", stdin);
// freopen("out_mine.txt", "w", stdout);
scanf("%d%d%d", &vertex_num, &query_num, &root);
for (int i = 1; i < vertex_num; i++)
{
int a, b;
scanf("%d%d", &a, &b);
G[a].push_back(b);
G[b].push_back(a);
}
for (int i = 0; i < query_num; i++)
{
int a, b;
scanf("%d%d", &a, &b);
query[a].push_back(QUERY{b, i}); //询问的另一个节点 & 询问编号
query[b].push_back(QUERY{a, i});
}
init(vertex_num); //并查集初始化
Tarjan(root, -1); //从 root 开始 Tarjan 离线回答询问;-1: 根节点没有父节点
for (int i = 0; i < query_num; i++)
{
printf("%d\n", ans[i]); //按之前离线处理的顺序回答询问
}
}
基于 RMQ 的算法
const int max_v = 5e5 + 10;
const int max_log_v = 100; // 2^20 足够囊括所有的点
vector<int> G[max_v]; //图的邻接表表示
int root; //有根树的`根节点`
int vertex_num; //树中节点的个数
//保存第 i 次 DFS 中 前序 & 后序遍历 访问到的节点信息
struct ORDER
{
int vertex_idx; //节点编号
int depth; //深度
} dfs_order[max_v * 2]; //由于 前序 & 后序遍历都要记录,所以要 *2
int vis_order[max_v]; //节点 i 第一次被 DFS 访问(前序遍历)是在 vis_order[i] 次
//预处理出的 `稀疏表`,f[k][i] 表示在`访问顺序`区间 [i, i+(1<<k)-1] 中 `depth` 的最小值 及其编号
pair<int, int> f[max_log_v][max_v * 2]; //其中 first 代表其区间最大值,second 表示其对应的节点编号
// DFS 前序 & 后序遍历,并记录节点的信息到 dfs_order 中
void dfs(int v, int parent, int d, int &k)
{ // vertex & parent & depth & k 用来记录访问顺序
vis_order[v] = ++k;
dfs_order[k] = ORDER{v, d}; //记录前序遍历
for (int i = 0; i < G[v].size(); i++)
{
const int &to = G[v][i];
if (to != parent)
{
dfs(to, v, d + 1, k);
dfs_order[++k] = ORDER{v, d}; //记录后序遍历
}
}
}
//`稀疏表`预处理
// len: 需要处理的长度
//这里是在维护 节点的访问顺序,使其后续能够在 O(1) 的时间内找出
//从第 st 到第 ed 个被访问的节点中深度最 `小` 的节点 `编号`
void sparse_table_init(int len)
{
//先导入节点数据到 f[0][i]
for (int i = 1; i <= len; i++)
{
f[0][i] = make_pair(dfs_order[i].depth, dfs_order[i].vertex_idx);
//第 i 个被访问的节点的深度 & 编号
// printf("f[0][%d]: %d %d\n", i, dfs_order[i].depth, dfs_order[i].vertex_idx);
}
// idx 代表当前的指数,从 1 (2^1 = 2) 开始
int logn = 31 - __builtin_clz(len);
for (int idx = 1; idx <= logn; idx++)
for (int pos = 1; pos + (1 << idx) - 1 <= len; pos++)
// pos 代表当前处理到的位置
f[idx][pos] = min(f[idx - 1][pos],
f[idx - 1][pos + (1 << (idx - 1))]); //递推(原理类似于 LCA 倍增)
}
//查询从 st 到 ed 中的 深度 `depth` 最小值对应的`下标`
inline int query_min_idx(int st, int ed)
{
int lg = 31 - __builtin_clz(ed - st + 1); //求出 st 到 ed 长度的二进制指数
//两个重合区间构成了整个 [st, ed] 区间,此时只需要返回这两个区间的最大值即可
if (f[lg][st] < f[lg][ed - (1 << lg) + 1])
return f[lg][st].second; //如果在左边区间深度较小
else //否则在右边区间深度较小,并返回节点的编号
return f[lg][ed - (1 << lg) + 1].second;
}
//预处理出 vs、depth 和 vis_order
void init(int v)
{
int k = 0;
dfs(root, -1, 1, k);
//预处理出 RMQ (返回的不是最小值,而是最小值对应的下标)
sparse_table_init(2 * v - 1); //前序遍历途径 v 个节点,后续遍历途径 v-1 个节点(每一条边都会导致后续遍历一次)
}
//计算 u 和 v 的 LCA
int lca(int u, int v)
{ // min 和 max 保证了传入的参赛 st <= ed
return query_min_idx(min(vis_order[u], vis_order[v]), max(vis_order[u], vis_order[v]));
}
template <typename T>
inline T read() // 声明 template 类,要求提供输入的类型T,并以此类型定义内联函数 read()
{
T sum = 0, fl = 1; // 将 sum,fl 和 ch 以输入的类型定义
int ch = getchar(); // fl 标记是否是负数
for (; !isdigit(ch); ch = getchar())
if (ch == '-')
fl = -1;
for (; isdigit(ch); ch = getchar())
sum = sum * 10 + ch - '0';
return sum * fl;
}
int main()
{
int query_num;
// scanf("%d%d%d", &vertex_num, &query_num, &root);
vertex_num = read<int>();
query_num = read<int>();
root = read<int>();
for (int i = 1; i < vertex_num; i++) //只有vertex-1条边
{
int p, b;
// scanf("%d%d", &p, &b);
p = read<int>();
b = read<int>();
G[p].push_back(b);
G[b].push_back(p);
}
init(vertex_num); //初始化
//求 x,y 的最近公共祖先
for (int i = 0, x, y; i < query_num; i++)
{
// scanf("%d%d", &x, &y);
x = read<int>();
y = read<int>();
printf("%d\n", lca(x, y));
}
return 0;
}
23树上差分
如果有一个区间内的权值发生相同的改变的时候,我们可以采用差分的思想方法
按点差分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkdpZZ5P-1663738769826)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220820075428311.png)]
按边差分
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0aEJv3Aa-1663738769826)(C:\Users\朱旭东\AppData\Roaming\Typora\typora-user-images\image-20220820075437985.png)]
24状态压缩DP、区间DP
状态压缩+动态规划 。既然是dp那么最为重要的就是找到状态转移方程然后转移就行。不同的在于状压dp利用二进制把状态记录成二进制数。
互不侵犯(状压DP)
f[i][j][s]=sum(f[i−1] [k] [s−gs[j]]),f[i] [j] [s]就表示在只考虑前i行时,在前i行(包括第i行)有且仅有s个国王,且第i行国王的情况是编号为j的状态时情况的总数。而k就代表第i-1行的国王情况的状态编号
#include<bits/stdc++.h>
using namespace std;
long long maze[10][1<<10][100];//记得要开long long
int f[1<<10],num[1<<10];
int check(int a,int b)
{//判断上下两行是否合法
if((a<<1)&b) return 0;
if((a>>1)&b) return 0;
if(a&b) return 0;
return 1;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int siz=1<<n;
for(int i=0;i<siz;i++)
{//预处理一行里有哪些状态是可行的
if(!(i&(i<<1))&&!(i&(i>>1)))
{
f[i]=1;
int tmp=i;
while(tmp)//预处理每种状态的国王数量
{
if(tmp&1) num[i]++;
tmp=tmp>>1;
}
}
}
for(int i=0;i<siz;i++)//由于第一行上面没有格子,所以需要单独处理
if(f[i]) maze[1][i][num[i]]=1;
for(int i=2;i<=n;i++)
{//枚举第几行
for(int j=0;j<siz;j++)//枚举这一行的状态
{
if(!f[j]) continue;
for(int k=0;k<siz;k++)//枚举上一行的状态
{
if(!check(k,j)||!f[k]) continue;
for(int l=m;l>=num[j];l--)
{
maze[i][j][l]+=maze[i-1][k][l-num[j]];
}
}
}
}
long long ans=0;
for(int i=0;i<siz;i++)
ans+=maze[n][i][m];
printf("%lld\n",ans);
system("pause");
return 0;
}
石子合并(区间DP)
区间dp就是利用了分治的思想,将整个区间不断的拆分一下,将一个区间[l,r]分成[l,k] [k+1,r],然后再对[l,k]和[k+1,r]进行类似的拆分,直到拆分成最小的区间
#include <iostream>
#include <cstring>
using namespace std;
const int N=310;
int n;
int a[N];
int s[N];
int f[N][N];
int int main(int argc, char const *argv[])
{
memset(f,0x3f,sizeof(f));
cin>>n;
for (int i = 1; i <=n; ++i)
{
cin>>a[i];//每堆石子的质量
s[i] = s[i-1]+a[i];//求前缀和
f[i][i] = 0;//合并每一堆石子的代价为0
}
for (int len = 2; len<=n; len++)
{
for (int l=1; l+len-1 <=n; ++l)
{
int r = l+len-1;
for (int k=l; k<r ; k++)
{
f[l][r] = min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[r-1]);
}
}
}
cout<<f[1][n]<<endl;
return 0;
}
环
#include<iostream>
#include<cstdio>
#include<cmath>
#include <cstring>
const int INF=999999999;
using namespace std;
int n,minl,maxl,f1[300][300],f2[300][300],num[300];
int s[300];
int main()
{
memset(f2,0x3f,sizeof(f2));
scanf("%d",&n);
for(int i=1;i<=n+n;i++)
{
scanf("%d",&num[i]);
num[i+n]=num[i];
s[i]=s[i-1]+num[i];
f2[i][i]=0;
}
for(int p=1;p<n;p++)
{
for(int i=1,j=i+p;(j<n+n) && (i<n+n);i++,j=i+p)
{
for(int k=i;k<j;k++)
{
f1[i][j] = max(f1[i][j], f1[i][k]+f1[k+1][j]+s[j]-s[i-1]);
f2[i][j] = min(f2[i][j], f2[i][k]+f2[k+1][j]+s[j]-s[i-1]);
}
}
}
minl=INF;
for(int i=1;i<=n;i++)
{
maxl=max(maxl,f1[i][i+n-1]);
minl=min(minl,f2[i][i+n-1]);
}
printf("%d\n%d",minl,maxl);
return 0;
}
25数论
矩阵
1、求解线性方程组
const int eps=1e-8;
typedef vector<double>vec;
typedef vector<vec>mat;//定义矩阵
vec gauss_jordan(const mat& A,const vec& b){//有解返回一个向量即x,无解或无穷多解返回长度为0的向量;
int n=A.size();
mat B(n,vec(n+1));
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)B[i][j]=A[i][j];
for(int i=0;i<n;i++)B[i][n]=b[i];
for(int i=0;i<n;i++){
int pivot=i;
for(int j=i;j<n;j++)
if(abs(B[i][j])>abs(B[pivot][i]))pivot=j;
swap(B[i],B[pivot]);
if(abs(B[i][i])<eps)return vec();//返回长度为0的向量vec
for(int j=i+1;j<=n;j++)B[i][j]/=B[i][i];
for(int j=0;j<n;j++){
if(i!=j){
for(int k=i+1;k<=n;k++)B[j][k]-=B[j][i]*B[i][k];
}
}
}
vec x(n);
for(int i=0;i<n;i++)x[i]=B[i][n];
return x;
}
2、快速幂与矩阵快速幂
快速幂:
解决 ab % c 计算过程爆longlong的问题
typedef long long ll;
ll qpow(ll a,ll b,ll mod){
ll ans=1;
while(b>0){
if(b&1)ans=(ans*a)%mod;
a=a*a%mod;
b>>=1;
}
return ans;
}
矩阵快速幂:
计算An,应用:通过把数放到矩阵的不同位置,然后把普通递推式变成"矩阵的等比数列",最后快速幂求解递推式
struct node {
int mat[15][15];//定义矩阵
}x,y;
node mul(node x,node y){//矩阵乘法
node tmp;
for(int i=0;i<len;i++){
for(int j=0;j<len;j++){
tmp.mat [i][j]=0;
for(int k=0;k<len;k++){
tmp.mat [i][j]+=(x.mat [i][k]*y.mat [k][j])%mod;
}
tmp.mat [i][j]=tmp.mat[i][j]%mod;
}
}
return tmp;
}
node matpow(node x,node y,int num){//矩阵快速幂 ,y为答案矩阵 num为幂
while(num){
if(num&1){
y=mul(y,x);
}
x=mul(x,x);
num=num>>1;
}
return y;
}
质数
试除法判断质数
int isprime(int n)
{
if(n == 1) return 0;
if(n == 2 || n == 3) return 1;
for (int i = 2; i*i <= n; i++)
if (n%i == 0)
return 0;
return 1;
}
埃式筛法打质数表
基本思想:初始将所有大于等于 2 的数放在一个集合中,每次筛选后集合中剩余最小的数是质数,将它的倍数去掉。
算法结束时,没有被筛去的数就是质数。每个数要被自己所有的因子标记一遍,所以普通筛的时间复杂度为O(nloglogn)
bool isprime[maxn];// 1 素数 0 和数
void sieve(int n){
for(int i=1;i<=n;i++) isprime[i]=1;
isprime[0]=isprime[1]=0;
for(int i=2;i*i<=n;i++){ //2-> 根号n
if(isprime[i]){
for(int j=2*i;j<=n;j+=i) isprime[j]=0;
}
}
}
//返回1~n-1中有多少个质数
int sieve(int n){
int num=0;//
for(int i=0;i<n;i++)isprime[i]=1;
isprime[0]=isprime[1]=0;
for(int i=2;i<n;i++){
if(isprime[i]){
num++;//加一点
for(int j=2*i;j<n;j+=i)isprime[j]=0;
}
}
return num;//
}
#include<iostream>
#include<stdio.h>
#include<cstring>
using namespace std;
const int maxn = 1e8;
bool isPrime[maxn + 5];
int cnt=0;
int main(){
long long n;
scanf("%lld",&n);
isPrime[1] = 1;
for(int i = 2; i*i <= n; i++)
if(isPrime[i]==0){
for(int j = i+i; j <= n; j+=i)
isPrime[j] = 1;
}
for(int i=1;i<=n;i++)
{
if(isPrime[i]==0)
cnt++;
}
printf("%lld\n",cnt);
return 0;
}
质数线性筛–欧拉筛
每个合数必为它的最小质因子和一个**最大因数(除它本身)**的乘积
const int maxn=120005;
int prime[maxn];//1~n存找到的素数
int num;//记录当前为止找到的素数个数
int vis[maxn];//是否访问过该数
void euler(int n){
memset(prime,0,sizeof(prime));
memset(vis,false,sizeof(vis));
scanf("%d",&n);
vis[1]=true;
for(int i=2;i<=n;i++){ //每个i
if(!vis[i]){
vis[i]=true;
prime[++num]=i;
}
for(int j=1;j<=num&&i*prime[j]<=n;j++){ //质数的i倍都删除
vis[prime[j]*i]=true;//筛除
if(i%prime[j]==0) break;
}
}
}
质数区间筛
//用于求出一段区间[a,b)内的全部质数
//O(Mlog(logn))m为区间长度,n为最大的r
//为避免a、b过大,可以用i-a来计入数组
bool isprime[maxm];//a->b
bool isprime0[maxm];//2->sqrt(b)
ll prime[maxn];//记录素数的值,方便调用
void segment_sieve(ll a,ll b){
//init:对[2,sqrt(b))内全部初始化为质数
for(ll i=0;i*i<b;i++)isprime0[i]=1;
//init:isprime[i-a]=1;
for(ll i=0;i<b-a;i++)isprime[i]=1;
for(ll i=2;i*i<b;i++){
if(isprime0[i]!=0){
for(ll j=2*i;j*j<b;j+=i)isprime0[j]=0;//筛选[2,sqrt(b));
//(a+i-1)/i得到最接近a的i的倍数,最低是i的2倍,然后筛选,两者取max
for(ll j=max(2LL,(a+i-1)/i)*i;j<b;j+=i)isprime[j-a]=0;
}
}
int num=0;
for(ll i=0;i<b-a;i++){
if(isprime[i])prime[num++]=i+a;
}
return;
}
int main()
{
ll a,b;
cin>>a>>b;
segment_sieve(a,b);
for(ll i=0;i<b-a;i++)
if(is_prime[i])
ans++;
cout<<ans<<endl;
return 0;
}
模运算
1.最大公约数gcd
要注意负数要转成正数再算 gcd
lcm=|a*b|/gcd(a,b)
int gcd(int a,int b){
return b==0?a:gcd(b,a%b);
}
裴蜀定理exgcd
要注意负数要转成正数再算 gcd \gcdgcd
又称贝祖定理,任意a,b不全为0的整数,存在整数x,y,使得:
a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)
可以将朴素欧几里得算法回带带出x,y证明 其正确性。
ll exgcd(ll a,ll b,ll &x,ll &y){
if(a==0&&b==0)return -1;
if(b==0){
x=1;y=0;
return a;
}
ll ans=exgcd(b,a%b,y,x);
y-=a/b+x; //注意这里做了修改 *->+
return ans;
}
2、求逆元
逆元定义
a*y %p ≡ 1%p,则y为a的逆元。//解决除法
求逆元
a y = 1 + m k ay=1+mk ay=1+mk a y − m k = 1 ay-mk=1 ay−mk=1 求y与k
此为ax+by=gcd(a,b) 模型,可以使用exgcd来解
int exgcd(int a,int b,int &x,int &y){
if(a==0&&b==0)return -1;//无解
if(b==0)x=1,y=0,return;
int ans=exgcd(b,a%b,y,x);
y-=a/b+x;
return ans;
}
//跑完mod_inverse后返回a的逆元,x中存"y"值,y中存"-k"的值
int mod_inverse(int a,int m){
int x,y;
exgcd(a,m,x,y);
return (m+x%m)%m;
}
//求在 mod=p 下 1~n 的逆元(打表)
void inv_arrey(int n, int p){
inv[1] = 1;
for (int i = 2; i <= n; ++i){
inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}
}
3、费马小定理
当p为素数时 ∀ x , x p ≡ x ( m o d p ) ∀x,x^p≡x(mod p) ∀x,xp≡x(modp)
∴ ∀无法被p整除的x, x ( p − 1 ) ≡ 1 ( m o d p ) x^{(p-1)}≡1(mod p) x(p−1)≡1(modp)
∴ x ( p − 2 ) ≡ x − 1 ( m o d p ) x^{(p-2)}≡x^{-1}(mod p) x(p−2)≡x−1(modp) 可以用此公式来计算逆元
即:
a
−
1
≡
a
p
−
2
(
m
o
d
p
)
a^{-1}≡a^{p-2}(mod p)
a−1≡ap−2(modp)
//费马小定理求逆元
//快速幂
ll binaryPow(ll base, ll expo, ll p){
if (expo == 0) return 1;
if (expo % 2 == 1)
return base * binaryPow(base, expo-1, p) % p;
else{
ll mul = binaryPow(base, expo/2, p) % p;
return mul % p * mul % p;
}
}
//在(mod p)的意义下,b的乘法逆元
ll inv(ll b) {return binaryPow(b, mod-2, mod);}
4、欧拉定理与欧拉函数
费马小定理中的mod数p必须为素数,推广后在欧拉定理与欧拉函数中,p可以是任意实数。
欧拉函数
欧拉函数的值表示[0,m-1]区间内与m互素的数的个数,规定φ(1)=1,而当m本身为素数时,φ(m)=m-1;
//给出m求φ(m) O(m^0.5)
int euler_phi(int m){
int res=m;
for(int i=2;i*i<=m;i++){
if(m%i==0){
res=res/i*(i-1);
for(;m%i==0;m/=i);
}
}
if(m!=1)res=res/m*(m-1);
return res;
}
//打表1~n的欧拉值,利用埃式筛
int euer[maxn];
void euler_phi(){
for(int i=0;i<maxn;i++)euler[i]=i;
for(int i=2;i<maxn;i++){
if(euler[i]==i){
for(int j=i;j<maxn;j+=i)euler[j]=euler[j]/i*(i-1);
}
}
return;
}
欧拉函数的性质:
Ⅰ.当p为素数,k>=1时有
φ
(
p
k
)
=
p
k
−
p
k
−
1
=
p
k
−
1
(
p
−
1
)
=
p
k
(
1
−
1
/
p
)
φ(p^k)=p^k-p^{k-1}=p^{k-1}(p-1)=p^k(1-1/p)
φ(pk)=pk−pk−1=pk−1(p−1)=pk(1−1/p)
Ⅱ.欧拉函数为积性函数,如果m和n互素,则
φ
(
m
n
)
=
φ
(
m
)
φ
(
n
)
φ(mn)=φ(m)φ(n)
φ(mn)=φ(m)φ(n)
欧拉定理
若m是正整数,gcd(k,m)=1,则
k
φ
(
m
)
≡
1
(
m
o
d
m
)
k^{φ(m)}≡1(mod m)
kφ(m)≡1(modm)
5、组合数
//阶乘+快速幂求逆元=求组合数
ll fac[maxn];//预处理阶乘表
void serFac(int n){
fac[0]=1;
for(int i=1;i<=n;++i){
fac[i]=1LL*fac[i-1]*i%mod;
}
}
ll binaryPow(ll a,ll b,ll m){
ll ans=1;
while(b){
if(b&1)ans=ans*a%m;
a=a*a%m;
b>>=1;
}
return ans;
}
ll C(int n,int m){
if(n<m)return 0;
if(n<0||m<0)return 0;
ll t=fac[n-m]*fac[m]%mod;
ll inv=binaryPow(t,mod-2,mod);
return fac[n]*inv%mod;
}
解同余方程
a x 0 ≡ b ( m o d m ) ax0\equiv b(mod m) ax0≡b(modm)
//解同余方程$ax0\equiv b(mod m)$中最小的x0,若无解返回false
bool ModularEqu(int a,int b,int m,int &x0){
int x,y,k;
int d=exgcd(a,m,x,y);
if(b%d==0){
x0=x*(b/d)%m;k=m/d;x0=(x0%k+k)%k;
return true;
}
return false;
}
26trie,kmp,字符哈希
字典树
规模取决于存储单词的数量和单词间的差异
#include<iostream>
#include<map>
#include<string.h>
#include<stdio.h>
using namespace std;
int main(){
map<string, int>s;
char a[20];
while(gets(a) && a[0] != '\0'){
for(int i = strlen(a);i>0;i--){
a[i]='\0';
s[a]++;
}
}
while(gets(a)){
cout<<s[a]<<endl;
}
return 0;
}
KMP
char A[1000006],B[1000006];//原串 模式串
int next[1000006];
void KMP(){
int la=strlen(A+1);
int lb=strlen(B+1);
int i,j;
//求出next数组
//b串自己与自己匹配 前缀为模式串 后缀为主串
//j是b串前缀的指针
next[1]=j=0;
for(i=2;i<=lb;i++){
while(j>0&&B[j+1]!=B[i]) j=next[j];
if(B[j+1]==B[i]) next[i]=++j;//匹配
}
j=0;
for(i=1;i<=la;i++){//进行匹配
while(j>0&&B[j+1]!=A[i]) j=next[j];//回退 j==0便忽略 然后增加i
if(B[j+1]==A[i])j++;//单个字符匹配成功
if(j==lb) {//全部匹配成功
printf("%d\n",i-lb+1);
j=next[j];
}
}
}
//https://vjudge.net/contest/512179#problem/B
#include<bits/stdc++.h>
using namespace std;
int n,k,len1,len2,t,ans=0,ans1[1000005];
int next[1000005];
char A[1000005],B[1000005];
void KMP(){
int la=strlen(A+1);
int lb=strlen(B+1);
int i,j;
next[1]=j=0;
for(i=2;i<=lb;i++){//求出next数组
while(j>0&&B[j+1]!=B[i]) j=next[j];
if(B[j+1]==B[i])j++;
next[i]=j;
}
j=0;
for(i=1;i<=la;i++){//进行匹配
while(j>0&&B[j+1]!=A[i]) j=next[j];//回退
if(B[j+1]==A[i])j++;
if(j==lb) {
ans1[ans]=i-lb+1,ans++;
j=next[j];
}
}
}
int main(){
cin>>t;
for(int i=0;i<t;i++){
ans=0;
memset(ans1,0,sizeof(ans1));
scanf("%s",A+1);
scanf("%s",B+1);
KMP();
if(ans==0){
cout<<"Not Found"<<endl<<endl;
}else{
cout<<ans<<endl;
for(int j=0;j<ans;j++){
cout<<ans1[j]<<" ";
}
cout<<endl<<endl;
}
}
return 0;
}
字符哈希
27博弈基础
巴什博弈
n个物品 两人轮流取1~m个 先取完者胜
n=k*(m+1)+r 先手取走r 必胜
n=k*(m+1) 先手必败
if(n%(m+1)) return false;
else return true
威佐夫博弈
两堆各有若干物品,可以在一堆拿若干(>1),也可以在两堆拿相同数量物品,先取完者胜
差值*黄金分割比==最小值,则后手赢
double r=(sqrt(5.0)+1)/2;
int d=abs(a-b)*r;
if(d!=min(a,b)) return true;
else return false;
尼姆博弈
有n堆石头,两人轮流从中取石头,每次取石头的个数>1,最后取完者胜
n堆物品数量全部异或,结果为0则必败,否则必胜
int res=0;
for(int i=1;i<=n;i++){
res=res^a[i];
}
if(res) return true;
else return false;
SG函数
有向无环图游戏
- mex{ 集合 } 是集合里未出现的最小非负整数
- 注意:
- 可选步数为1-m的连续整数,直接取模即可,SG(x) = x % (m+1);
- 可选步数为任意步,SG(x) = x;
- 可选步数为一系列不连续的数,用mex(计算每个节点的值)
【HDU1848】
#include<iostream>
#include<cstring>
using namespace std;
const int Maxm=1010,Maxn=10;
int f[20],sg[Maxm];
bool Hash[Maxm];
void Getsg()
{
f[0]=1;f[1]=1;
for(int i=2;i<=16;i++){//得到斐波那契数列
f[i]=f[i-1]+f[i-2];
}
for(int i=1;i<Maxm;i++){//得到sg
memset(Hash,false,sizeof(Hash));
for(int j=1;j<=16&&f[j]<=i;j++){
Hash[sg[i-f[j]]]=true;
}
for(int j=0;j<=i;j++){
if(!Hash[j]){
sg[i]=j;
break;
}
}
}
}
int main()
{
Getsg();
int m1,m2,m3;
while(scanf("%d%d%d",&m1,&m2,&m3)==3&&m1){
if((sg[m1]^sg[m2]^sg[m3])!=0){
cout<<"Fibo"<<endl;
}
else{
cout<<"Nacci"<<endl;
}
}
}
28背包DP,树上DP (树上背包)
0 1 背包
【背包容量由大到小进行更新】
我们遍历每一种物品,每种物品都有取或不取两种选择。
初始化:dp[0] [i] = 0, 其中 0 ≤ i ≤ m
目标值:dp[n] [m]
int dp[maxn][maxm];//用 dpi 表示遍历到第 i 件物品,且背包容量为 j 时的最大总价值。
void init() {//初始化
for(int i = 0; i <= m; i++) {
dp[0][i] = 0;
}
}
for(int i=1;i<=n;i++){//遍历每一件物品
for(int j=m;j>=0;j--){//背包重量递减
//如果放不下了肯定不能硬塞
if(j < w[i]) {
dp[i][j] = dp[i - 1][j];
}
//如果能放的下
else {//就看看要不要放入这件物品
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]); }
}
}
}
printf("%d", dp[n][m]);//输出答案
改进代码:
int dp[maxn];//表示容量为maxn是的最大价值
memset(dp,0,sizeof(dp));//初始化
for(int i=1;i<=n;i++ ){//遍历每一种物品
for(int j=m;j> w[i];j--){{//注意j一定要由大到小更新,且下界为w[i]
dp[j]= max(dp[j], dp[j - w[i]] + v[i]);//dp[j]容量为j时不拿第i件物品 dp[j - w[i]] + v[i] 容量为j时拿第i件物品
}
}
printf("%d",dp[m]);
完全背包
与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次
dp[i] [j]:表示前i件物品放入容量为j的背包的最大价值
背包重量从小到大枚举
dp[i] [j] = max(dp[i-1] [j], dp[i] [j-w[i]]+v[i])
for(int i=1;i<=n;++i) //遍历物品数量
{
for(int j=0;j<=t;++j) //遍历背包容量 递增
{
dp[i][j] = dp[i-1][j]; //继承上一个背包
if(j>=v[i])
{
//完全背包状态转移方程
dp[i][j] = std::max(dp[i-1][j],dp[i][j-v[i]]+w[i]);
}
}
}
改进:
for(int i=1;i<=n;++i) //遍历物品数量
{
for(int j=0;j<=t;++j) //遍历背包容量
{
dp[i] = dp[j]; //此时右边的dp[j]是上一层i-1的dp[j],然后赋值给了当前i的dp[i]
if(j>=v[i])
{
//完全背包状态转移方程
dp[j] = std::max(dp[j],dp[j-v[i]]+w[i]);
}
}
}
多重背包
与 0-1 背包的区别在于每种物品有 k个,而非一个
dp[i] [j]:表示前i件物品放入容量为j的背包的最大价值
dp[i][j] = dp[i-1] [j-k*v[i]] + k *w[i]
for(int i=1;i<=n;i++) //遍历物品数量
{
for(int j=0;j<=m;j++) //遍历背包容量
{
for(int k=0;k<=ki && k*v[i]<=j;k++) //遍历每个物品的件数
{
//k*v[i]:不能超过背包总容量
dp[i][j]=std::max(dp[i-1][j],dp[i-1][j-k*v[i]]+k*w[i]);
}
}
}
混合背包
有的只能取一次,有的能取无限次,有的只能取 k次。
树上背包
满足如果选取节点 v,则其所有祖先节点 u 都要选择的限制
边权题胚
给定一棵树。每条边有对应的非负权值 val 。
现今需要剪枝。给定需要保留的边的数量 m,求所剩边的最大权值之和。
点权题胚
给定一棵树。每个节点有对应的非负权值 val。
现今需要剪枝。给定需要保留的节点数量 m,求所剩节点的最大权值之和。
void dfs(int x,int father)
{
for(int i=h[x];i!=-1;i=ne[i])//用链式前向星存树
{
int j=e[i];
//树是一种有向无环图
//只要搜索过程中不返回父亲节点即可保证不会重复遍历同一个点。
if(j==father) continue;
dfs(j,x);//继续搜索下一个节点
}
}
void dfs(int u,int fa)
{
for(int i=head[u];i;i=e[i].pre)
{
int y = e[i].to;
if(y==fa) continue;
dp[y][1] = e[i].w; //把权重放在孩子节点
dfs(y,u); //向下递归
for(int j=m+1;j>=1;--j) //根节点必选,故j>=1
{
for(int k=0;k<j;++k)
{
dp[u][j] = std::max(dp[u][j],dp[u][j-k]+dp[y][k]);
}
}
}
}
树的直径
int d[maxn]; //d[x]:节点x到其子孙节点的最大距离
int f[maxn]; //f[x]:以x为根结点的一条最长路径的距离
void dfs(int u,int fa)
{
for(int i=head[u];i;i=e[i].pre)
{
int v = e[i].to;
int w = e[i].w;
if(v==fa) continue;
dfs(v,u);
f[u] = max(max(f[u],d[u]),max(d[v]+w,d[u]+d[v]+w));
d[u] = max(max(d[u],w),d[v]+w);
}
}
最大子树和
给定一棵有 n 个点的树,树上每个点有各自的点权。(存在负值)
在树中选取一棵子树,使得其权值之和最大。
设 dp[i] 表示以 i 为根节点的最大子树和。
dp[u] = val[u] + ∑max(dp[to], 0)
void dfs(int u, int father) {
dp[u] = val[u]; //初始化
for(int i = head[u]; i != -1; i = edge[i].next) { //遍历u的所有边
int to = edge[i].to; //取出节点
if(to == father) { //不要回头搜
continue;
}
dfs(to, u); //递归过程
if(dp[to] > 0) { //不使答案劣化
dp[u] += dp[to]; //才参与贡献
}
}
}
dfs(1, -1); //根节点和它不存在的父节点
int ans = ninf; //负无穷
for(int i = 1; i <= n; i++){
ans = max(ans, dp[i]);
}
printf("%d", ans); //输出
最大(权值)独立集
最大独立集定义:
对于一棵有 n 个结点的树,选出尽量多的结点,使得任两个结点均不相邻。
给定一棵有 n 个结点的树,及每个节点的权值 val
选择合适的节点,使得节点权值之和最大,并且任两节点不相邻。
设 dp[i][0] 表示不选择节点 i 时,以节点 i 为根的子树的最大独立集的权值。
设 dp[i][1] 表示选择节点 i 时,以节点 i 为根的子树的最大独立集的权值。
那么转移方程有:
i. dp[u] [0] = ∑max(dp[to] [0], dp[to] [1])
ii. dp[u] [1] = val[u] + ∑dp[to] [0]
void dfs(int u, int father) {
//初始化
dp[u][0] = 0;
dp[u][1] = val[u];
for(int i = head[u]; i != -1; i = edge[i].next) {
int to = edge[i].to;
if(to == father) {
continue;
}
dfs(to, u);
dp[u][0] += max(dp[to][0], dp[to][1]);
dp[u][1] += dp[to][0];
}
}