高精度乘法
反向输入,123*2-246 C:642
设置一个进位t,正向处理每一 位,每- -次都是加 上然后把的个位塞进C,最后反相输入
#include<vector>
using namespace std;
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;
}
return C;
}
int main()
{
string a;
int b;
cin>>a>>b;
vector<int>A,C;
for(int i=a.size() -1;i>=0;i--)
{
A.push_back(a[i]-'0');
}
C=mul(A,b);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
二维差分
解决的是对于一个矩形区间内加上- -个数字,左上角+C,右下角+C,其他都是-C
最后和起来的时候减去左上角
求一个独立区间的,是用右下角-- +左上角
#include<iostream>
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N],b[N][N];
void insert(int x1,int y1,int x2,int y2,int c)
{
b[x1][y1]+=c;
b[x1][y2+1]-=c;
b[x2+1][y1]-=c;
b[x2+1][y2+1]+=c;
}
int main()
{
scanf("%d%d%d",&n,&m,&q);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
insert(i,j,i,j,a[i][j]);
}
}
while(q--)
{
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
insert(x1,y1,x2,y2,c);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
printf("%d",b[i][j]);
puts(" ");
}
return 0;
}
双指针
解决前面后面某个单调序列里面的关系问题
往往原型是两重循环
变成一重循环+while(j<=i&&check(j,i))j++
举个例子:最长连续不重复子序列
#include<iostream>
using namespace std;
const int N=100010;
int a[N],n;
int s[N];
int main()
{
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
int res=0;
for(int i=0,j=0;i<n;i++)
{
s[a[i]]++;
while(s[a[i]]>1)
{
s[a[j]]--;
j++;//j用来删掉重复的
}
res=max(res,i-j+1);
}
cout<<res<<endl;
return 0;
}
位运算.
x&-x
n>>i&1
单调队列
53 543 2<5- 3+1 i-k+1>q[h h]
t=5 t=4 q[5] ql++t]=I;
用在在某一前一定范围内取这一范围内最大最小值, h往往是要用的值(最小or最大)
应用:滑动窗口
#include<iostream>
using namespace std;
const int N=100010;
int a[N],q[N];
int n,k;
int main()
{
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
int hh=0,tt=-1;
for(int i=-1;i<n;i++)
{
//判断对头是否画出
if(hh<=tt&&i-k+1>q[hh])hh++;
while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) printf("%d",a[q[hh]]);
}
puts(" ");
}
Kmp
类似于双指针,i=2,j=0 预处理那个短的
While p[i]!=p[i+1]j=ne[j];
(if(i==pl[+1])j++
next[i]=j;
#include<iostream>
using namespace std;
const int N=10010,M=100010;
int n,m;
char p[N],s[N];
int ne[N];
int main()
{
cin>>n>>p+1>>m>>s+1;
//求next
for(int i=2,j=0;i<=n;i++)
{
while(j&&p[i]!=p[j+1])j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j; }
//kmp匹配过程
for(int i=1,j=0;i<=m;i++)
{
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n)
{
printf("%d",i-n+1);
j=ne[j];
}
}
return 0;
}
离散化.
- 无序: get (x) if(S.count(x)= =0)S[x]=++n;unordered. map
- 有序:ally.push_ back()
sort(ally .begin(,ally .end())
ally .erase (unique(ally .begin(), ally .end() ),ally .end())
x=find(x);find: 用二分
高精度除法
4567/12
反向输入7654
设置一个r 反向输出r*10+ a[i] temp正向存入r/b 然后反向去前导零,再反向输出(我觉
得可以之间返回了)
整数二分找到第一个等于这个数的位置q[mid]> =x r= mid;
找到最后一个
q[mid]<=x |=mid
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
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;
}
int main()
{
string a;
int b;
cin>>a>>b;
vector<int>A,C;
for(int i=a.size() -1;i>=0;i--)
{
A.push_back(a[i]-'0');
}
int r;
C=div(A,b,r);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
cout<<endl;
cout<<r<<endl;
return 0;
}
高精度加法
字符串读入并且把每一位转化成数字存入数组 12345=>54321
从第一位开始增加,设置一个进位t,c里面存t%10
最后如果t还有的话要向答案里push1
最后反向输出C
#include<iostream>
#include<vector>
using namespace std;
const int N=1e6+10;
vector<int> add(vector<int> &A,vector<int> &B)
{
vector<int> C;
int t=0; //进位
for(int i=0;i<A.size() ||i<B.size() ;i++)
{
if(i<A.size())t+=A[i];
if(i<B.size())t+=B[i];
C.push_back(t%10);
t/=10;
}
if(t) C.push_back(1);
return C;
}
int main()
{
string a,b;
vector<int> A,B,C;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--)B.push_back(b[i]-'0');
C=add(A,B);
for(int i=C.size()-1;i>=0;i--)
{
// printf("%d",C[i]);
cout<<C[i];
}
return 0;
}
高精度减法
设置一个t,每一次t=减数-t,再用t去减被减数,如果t<0,t=1
#include<iostream>
#include<vector>
using namespace std;
const int N=1e6+10;
vector<int> add(vector<int> &A,vector<int> &B)
{
vector<int> C;
int t=0; //进位
for(int i=0;i<A.size() ||i<B.size() ;i++)
{
if(i<A.size())t+=A[i];
if(i<B.size())t+=B[i];
C.push_back(t%10);
t/=10;
}
if(t) C.push_back(1);
return C;
}
int main()
{
string a,b;
vector<int> A,B,C;
cin>>a>>b;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--)B.push_back(b[i]-'0');
C=add(A,B);
for(int i=C.size()-1;i>=0;i--)
{
// printf("%d",C[i]);
cout<<C[i];
}
return 0;
}
Trle树
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];
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];
}
哈希函数
- 拉链法:
e[idx]=x;ne[idx]=h[k]
h[k]=idx++;
If e[x]==x return true;
- 开放寻址法
while(h[k]!=null&h[k]! =x)
{k+ +;i(k= =N)k=0;}
return k;
字符串:选择P=131 13331
用一个P进制for (int i=1;i<=n;i++)
字符串哈希
用p进制(131,13331)
for(int i=1;i<=n;i++)
{
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+str[i]
}
get :return h[r]-h[l-1]*p[r-l+1]
堆排序(小顶堆)
void up
while(u/2&&h[u/2]>h[u])
{ heap_swap(u/2,u)
u/=2;}
void down
int t=u;
if(u*2<=size&&h[u*2]<h[t])t=u*2;
if(u*2+1<=size&&h[u*2+1]<h[t])t=u*2+1;
if(u!=t){heap_swap(u,t),down(t);}
八皇后
dfs参数是行数,则函数内部,对于列进行深搜,每选择一个列,满足竖斜斜不矛盾
void dfs(int u)
{
if(u==n)
{
for(int i=0;i<n;i++)
{
puts(g[i]);
}
puts("");
return ;
}
for(int i=0;i<n;i++)
{
if(!col[i]&&!dg[u+i]&&!udg[n-u+i])
{
g[u][i]='Q';
col[i]=dg[u+i]=udg[n-u+i]=true;
dfs(u+1);
col[i]=dg[u+i]=udg[n-u+i]=false;
g[u][i]='.';
}
}
}
dfs参数是坐标和皇后的个数,就是一个一个的枚举放不放
y不断增加,直到n,然后清零,x++,
不放皇后(x,y+1,s)
如果满足条件 :横竖两斜 dfs(x,y+1,s+1)
void dfs(int x,int y,int s)
{
if(y==n)y=0,x++;
if(x==n)
{
if(s==n)
{
for(int i=0;i<n;i++)
puts(g[i]);
puts("");
}
return ;
}
//不放皇后
dfs(x,y+1,s);
//放皇后
if(!row[x]&&!col[y]&&!dg[x+y]&&!udg[x-y+n])
{
g[x][y]='Q';
row[x]=col[y]=dg[x+y]=udg[x-y+n]=true;
dfs(x,y+1,s+1);
row[x]=col[y]=dg[x+y]=udg[x-y+n]=0;
g[x][y]='.';
}
}
树
不能忘记 memset(h,-1,sizeof h)
树的重心
深搜到叶结点,然后利用返回值跟新答案
int s=dfs(j)
res=max(res,s)
sum+=s;
res=max(res,n-sum)
ans=min(ans,res)
bfs
用队列去存储数据
先要设置h,t,q[0],d
while循环内,每次取出队头,进行操作,存入队列
寻找路径
Prev[x][y]=t
x=目标,y=目标
while(x||y)
{ cout<<x<<" "<<y;
PII t=Prev[x][y];
x=t.first,y=y.second}
bellman
用于存在负权求单源最短路(当限制了边数,就用这个)
思想:用边存两个端点和长度的信息
把所有的距离最大化,dist[1]=0
循环可以拓展的次数,每一次要把上一次的dist备份给backup,循环所有边数,到to顶点的距离被更新(防止被一次又一次更新)
如果dist[n]>0x3f3f3f3f/2,相当于无法到达
int bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=k;i++)
{
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++)
{
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
if(dist[n]>0x3f3f3f3f/2)return -1;
return dist[n];
}
朴素版dijkstra
稠密图,所有边权为正,邻接矩阵
每次寻找不在S中距离最近的点作为下一个拓展的中间点,用它去进行更新
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
{if(!st[j]&&(t==1||dist[t]>dist[j]);t=j;}
st[t]=true;
for(int i=1;i<=n;i++)
{dist[j]=min(dist[j],dist[t]+g[t][j]);}
if(dist[n]==0x3f3f3f3f)return -1;
return dist[n];
堆优化版本dijkstra
优先队列当堆 存着pair <到源距离,哪个点>
只需要一个循环,每一次去拉伸,存入,由于是优先队列,所以会自然按照距离排序,如果这个点已经被收入了就跳过
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII> >heap;
heap[0]={0,1};
while(heap.size())
{PII t=heap.top();
heap.pop();
int distance =t.firse,ver=t.second;
if(st[ver])continue;
st[ver]=1;
for(int i=h[[ver];i!=-1;i=ne[i])
{ int j=e[i];
if(dist[j]>distance+w[i])
{ dist[j]=distance+w[i];
heap.push({dist[j],j};
}
}
SPFA
可以用于有负权的最小路
使用队列 每一次是自己相连的几个点去拓展,先拓展,如果这个被拓展且不在队列就加入队列,所以可以每个点拓展无数次,达到各种目的
int spfa()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
queue<int>q;
q.push(1);
st[1]=1;
while(q.size())
{
int t=q.front();
q.pop();
st[t]=false;
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(!st[j])
{
q.push(j);
st[j]=true;
}
}
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
kruscal
用于稀疏图·
用边存两个端点,和距离
对于距离进行排序,看最小边的两个端点是不是属于一个树,不是的话就把他们合并,利用并查集,如果最后连接的边数<n-1,证明无法形成树
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m;
int p[N];
struct Edge
{
int a,b,w;
bool operator<(const Edge &W)const
{
return w<W.w;
}
} edges[N];
int find(int x)
{
if(x!=p[x])p[x]=find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++)
{
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edges[i]=(Edge){a,b,w};
}
sort(edges,edges+m);
for(int i=1;i<=n;i++)
p[i]=i;
int res=0,cnt=0;
for(int i=0;i<m;i++)
{
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
a=find(a),b=find(b);
if(a!=b)
{
p[a]=b;
res+=w;
cnt++;
}
}
if(cnt<n-1)puts("impossible");
else printf("%d\n",res);
return 0;
}
prim
类似于dijkstra,用于存储稠密图
int prim()
{
memset(dist,0x3f,sizeof dist);
int res=0;
for(int i=0;i<n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dist[t]>dist[j]))
t=j;
if(i&&dist[t]==INF) return INF;
if(i) res+=dist[t];
for(int j=1;j<=n;j++)
dist[j]=min(dist[j],g[t][j]);
st[t]=1;
}
return res;
}
染色法二分图
用于判断给出的一组点一张图能否进行二分,循环所有的点,对于没有被染色的点进行深搜
深搜是先把它涂色,对于它的连的边里没有染过色的,进行深搜,如果它的子结点和他是一个颜色,或者他子节点的子结点和他是一个颜色(存在奇数环,等的就是绕回自己的时候,else if就是给最后一个准备的,然后就会不断返回负值)
for(int i=1;i<=n;i++)
if(!color[i])
{if(!dfs(i,1)
break;
}
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])
{
if(!dfs(j,3-c)) return false;
}
else if(color[j]==c)return false;
}
}
匈牙利算法
一一匹配
对于左边所有点循环,每一次把st设为false
进行寻找
寻找过程,循环可以的配对对象,如果这个st是false,再去看是否配对了,或者原本对象可以再找一个,就把他配对
bool find(int x)
{
for(int i=h[x];i!=-1;i=ne[i])
{
int j=e[i];
if(!st[j])//st是为了让前夫哥寻找的时候pass掉找自己其他的
{
st[j]=true;
if(match[j]==0||find(match[j]))
{
match[j]=x;
return true;
}
}
}
return false;
}
约数个数
一系列数字乘积的约数个数,每个约数选择的可能性相乘
用无序表存储个数和对应的数字
int main()
{
int n;
cin>>n;
unordered_map<int,int>primes;
unordered_map<int,int>::iterator prime;
while(n--)
{
int x;
cin>>x;
for(int i=2;i<=x/i;i++)
while(x%i==0)
{
x/=i;
primes[i]++;
}
if(x>1)prime[x]++;
}
ll res=1;
for(prime=primes.begin();prime<=primes.end();prime++)
{
res=res*(prime.second+1)%mod;
}
cout<<res<<endl;
}
质数
普通筛法
for(int i=2;i<=n/i;i++)
{
if(n%i==0)
{
int s=0;
while(n%i==0)
{
n/=i;
s++;
}
cout<<i<<s;
}
if(n>1)cout<<n<<1;
}
埃式筛法
找到一个质数,然后接下去每+这个质数,肯定不是质数,标记了存在
for(int i=2;i<=n/i;i++)
{if(!st[i])
{ primes[cnt++]=i;
for(int j=i*i;j<=n;j+=i)
st[j]=1;
}
}
线性筛法
根据最小质因数去筛
for(int i=2;i<=n;i++)
{ if(!st[i])
{ primes[cnt++]=i;
for(int j=0;primes[j]<=n/i;j++)
{
st[primes[j]*i]=1;
if(i%primes[j]==0)break;
}
}
}
欧拉函数
1-N中与N互质的数的个数成为欧拉函数
N=p1^a1 *p2 ^a2 pm ^am
函数=N(p1-1/p1)(p2-1/p2)(pm-1/pm)
int a;
cin >> a;
int res = a;
for (int i = 2; i <= a / i; i++)
if (a % i == 0)
{
res = res / i * (i - 1);//为了保证是个整数,从:res=res*(1-1/i)而来
while (a % i == 0)a /= i;
}
if (a > 1) res = res / a * (a - 1);
cout << res << endl;
欧拉定理
a与n互质,则a的n的欧拉函数次幂模上n是1,n是质数,欧拉函数是n-1
快速幂
a的k次方mod p
int qmi(int a,int k,int p)
{int res=1;
while(k)
{if(k&1)res*=a%p;
k>>=1;
a=(ll)a*a%p;
}
return res;
}
欧几里得以及其扩展
int gcd(int a,int b)
return b?gcd(b,a%b):a;
扩展欧几里得
裴蜀定理:有任意一对正整数a,b,那么一定存在非零整数x,y,使得ax+by=gcd(a,b)
int exgcd(int a,int b,int &x,int &y)
{
if(!b){x=1,y=0;return a;}
int d=exgcd(b,a%b,y,x);
y-=a/b*x;
return d;
}
高斯消元
解的个数:无解 有一个n==0
一解 正好
无数解 少一个条件
计算顺序 循环每一列
找到绝对值最大的那一行,把这一 行换到最顶端
将这一行第一个数字变成1
通过减法,把下面每一行的这一列变成0
int guess()
{
int c,r;
for(c=0,r=0;c<n;c++)
{
int t=r;
for(int i=r;i<n;i++)
{
if(fab(a[i][c])>fabs(a[t][c]))
t=i;
}
if(fabs(a[t][c])<eps)continue;
for(int i=c;i<=n;i++)swap(a[t][i],a[r][c]);
for(int i=n;i>=c;i--)a[r][i]/=a[r][c];
for(int i=r+1;i<n;i++)
if(fabs(a[i][c])>eps)
for(int j=n;j>=c;j--)
a[i][j]-=a[i][c]*a[r][j];
r++;
}
if (r < n)
{
for (int i = r; i < n; i++)
{
if (fabs(a[i][n] > eps))
return 2;//无解
}
return 1;//五穷解
}
for (int i = n - 1; i >= 0; i--)
{
for (int j = i + 1; j < n; j++)
a[i][n] -= a[i][j] * a[j][n];
}
return 0;
}
组合数
10w组 1-2000
用递推公式
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
{ if(!j)c[i][j]=1;
else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
1w组,1-1e5
预处理出来阶乘
根据欧拉定理 a^p-2 * p(modp)为1,所以p的倒数就是a^p-2
fact[0]=infact[0]=1;
for(int i=1;i<N;i++)
{ fact[i]=fact[i-1]*i%mod;
infact[i]=infact[i-1]*qmi(i,mod-2,mod)%mod;
}
最后输出 fact[a]*infact[b]*infact[a-b]
20组,1-10^18
卢卡斯
C b a=C bmodp amodp *C b/p a/p
int qmi(int a, int k)
{
int res = 1;
while (k)
{
if (k & 1)res = (ll)res * a % p;
a = (ll)a * a % p;
k >>= 1;
}
return res;
}
int C(int a, int b)
{
int res = 1;
for (int i = 1, j = a; i <= b; i++, j--)
{
res = (ll)res * j % p;
res = (ll)res * qmi(i, p - 2) % p;
}
return res;
}
int lucas(ll a, ll b)
{
if (a < p && b < p)return C(a, b);
return (ll)C(a % p, b % p) * lucas(a / p, b / p) % p;
}
高精度
如果直接算效率太低
分解成p1^a1* p2^a2* pm^am的格式
然后上面减去下面的指数得到最终的
先筛素数,求出每个素数出现的个数 高精度计算
get_primes函数获取a的质因数
void get_primes(int n)
{
for (int i = 2; i <= n; i++)
{
if (!st[i])primes[cnt++] = i;
for (int j = 0; primes[j] <= n / i; j++)
{
st[primes[j] * i] = true;
if (i % primes[j] == 0)break;
}
}
}
gete函数获得指数
int gete(int n, int p)
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
高精度乘法 res*prime[i]
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;
}
return c;
}
卡特兰数
给定n个0,1按照排列成长度为2n的序列,求他们能排列成的所有序列中,
满足任意前缀序列中0的个数不少于1的序列
用于求前缀中两个数不小于关系
C n 2n -C n-1 2n
C n 2n /n+1
int a=2*n,b=n;
for(int i=a;i>a-b;i--)res=(res)*i%mod;
for(int i=1;i<=b;i++)res=(ll)res*qmi(i,mod-2,mod)
res=(ll)res*qmi(n+1,mod-2,mod)%mod;
cout<<res<<endl;
用递推
1 1 2 5 14 42 132
int a[20]={1};
for(int i=1;i<20;i++)
{
for(int j=0;j<i;j++)
{a[i]+=a[j]*a[i-j-1];}
}
容斥原理
给定一个整数n和m个不同的质数p1,p2…求出1-n中能被p1-pm中至少一个数整除的整数有几个
求n个集合的并集==一个的全集-两个的全集+三个的全集-四个的全集
位运算的优化
1~1<<m-1 表示了每一个质数选还是不选,j表示第几位
cin >> n >> m;
for (int i = 0; i < m; i++)
{
cin >> p[i];
}
int res = 0;
for (int i = 1; i < 1 << m; i++)//一共有2^m-1个数字,i看成一个二进制数,有n位,位上是0表示集合没有被选,1表示被选了
{
int t = 1, cnt = 0;//cnt表示包含几个1,有几个集合
for (int j = 0; j < m; j++)
{
if (i >> j & 1)
{
cnt++;
if ((ll)t * p[j] > n)//不用算了c
{
t = -1;
break;
}
t *= p[j];
}
}
if (t != -1)
{
if (cnt % 2)res += n / t;
else res -= n / t;
}
}
cout << res << endl;
Nim游戏
有向图游戏:有向无环,唯一起点,两名玩家交替把棋子延有向边移动
nim:给定N个物品,第i堆Ai个,2名玩家轮流获取,每次到任意一堆获取任意多物品,取走最后一件获胜
先手必胜状态:可以走到某个必败状态
a1 ^ a2 ^a3 ^a4=0先手必败 !=0 先手必胜
/*给定n堆石子,两个玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子*/
#include<iostream>
using namespace std;
int main()
{
int n;
int res = 0;
scanf_s("%d", &n);
while (n--)
{
int x;
scanf_s("%d", &x);
res ^= x;
}
if (res)puts("yes");
else puts("no");
}
SG函数
有n张图,则SG(x1) ^ SG(x2) ^SG(xn)!=0 必胜,每一个是这张图的起点
sg内部相当于分叉图,每一次存入数,然后从最后的回溯上来,进行mex运算
int sg(int x)
{if(f[x]!=-1)return f[x];
unordered_set<int>S;
for(int i=0;i<m;i++)
{ int sum=s[i];
if(x>=sum)S.insert(x-sum[i]);
}
for(int i=0;i;i++)
if(S.count(i)
return f[x]=i;
}
int main()
{
cin >> m;
for (int i = 0; i < m; i++)cin >> s[i];
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
for (int i = 0; i < n; i++)
{
int x;
cin >> x;
res^= sg(x);
}
if (res)puts("yes");
else puts("no");
return 0;
}
各种简单背包
分组背包
/*每组物品有若干,同一组中最多只选一个*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N][N], w[N][N],s[N];
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
for (int j = 0; j < s[i]; j++)
{
cin >> v[i][j] >> w[i][j];
}
}
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = 0; k < s[i]; k++)
{
if (v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m] << endl;
return 0;
}
01背包
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10010;
int m, n;
int v[N], w[N], f[N][N],dp[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for(int j=m;j>=v[i];j--)
{
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
cout << dp[m] << endl;
return 0;
}
完全背包
for (int i = 1; i <= n; i++)
{
for (int j = v[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
多重背包 每个物品可选有限个
for (int i = 1; i <= n; i++)cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i++)
{
for (int j = 0; j <= m; j++)
for (int k = 0; k <= s[i] && k * v[i] <= j; k++)
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
}
二进制优化,
把这些全部搞成不同类型背包,然后用01背包做,对k下文章
while(k<=s)
{ cnt++;
v[cnt]=a*k;
w[cnt]=b*k;
s-=k;
k*=2;
}
if(s>0)
{cnt++;
v[cnt]=a*s;
w[cnt]=b*s;
}
接下去n=cnt,做01背包
状态压缩
蒙德里安的梦想
自己这一排有限制,和前面一排也有关系
现予处理自己这一排不能有奇数个空,一共有1<<n种可能性,st[i]=true/false;
f[i][j]表示第i列,伸出来的小方块是j的情况数,由f[i-1][k]转移而来,j和k形成的也得满足不能有奇数个空格,同时不能有三个连着的冲突(j&k==0)
while (cin >> n >> m, n || m)
{
memset(f, 0, sizeof f);
for (int i = 0; i < 1 << n; i++)//预处理对于j的二进制数的0的个数,这个是保证了i+1行里面突出之间有偶数个
{
st[i] = true;
int cnt = 0;
for (int j = 0; j < n; j++)
{
if (i >> j & 1)
{
if (cnt & 1)st[i] = false;//cnt有奇数个就不可以有这样的决策
cnt = 0;
}
else cnt++;//当前连续零个数
}
if (cnt & 1) st[i] = false;
}
f[0][0] = 1;
for (int i = 1; i <= m; i++)
{
for (int j = 0; j < 1 << n; j++)
{
for (int k = 0; k < 1 << n; k++)
{
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
}
}
}
cout << f[m][0] << endl
区间dp
合并和分割其实区别不大
都是先循环区间长度,在循环区间左端点,再枚举决策·
表示从第i个到第j堆合并成一堆的方式min
for (int len = 2; len <= n; len++)//区间长度从小到大循环
{
for (int i = 1; i + len - 1 <= n; i++)//循环区间左端点
{
int l = i, r = len + i - 1;
f[l][r] = 1e8;
for (int k = 1; k < r; k++)//枚举决策
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
分割
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=1010;
int f[N][N];
int n,m;
int a[N];
int s[N];
int main()
{
while(cin>>n,n)
{
cin>>m;
memset(f,0x3f,sizeof f);
m++;
for(int i=1;i<m;i++)cin>>a[i];
a[m]=n;
for(int i=1;i<=m;i++)
{
s[i]=a[i]+s[i-1];
f[i][i]=0;
}
for(int len=1;len<m;len++)
for(int l=1;l+len<=m;l++)
{
int r=l+len;
for(int k=l;k<r;k++)
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
cout<<"The minimum cutting is "<<f[1][m]<<"."<<endl;
}
}
最长上升子序列
枚举此之前的为倒数第二,再次之后+1
for(int i=1;i<=n;i++)
{
f[i]=1;
for(int j=1;j<i;j++)
if(a[j]<a[i])
if(f[i]<f[j]+1)
f[i]=f[j]+1;
}
用一个单调队列
二分寻找最大的小于等于这个数的,把它后面一位顶替了,最后输出这个队列长度
for(int i=0;i<n;i++)
{ int l=0,r=len;
while(l<r)
{ int mid=l+r+1>>1;
if(q[mid]<a[i])l=mid;
else r=mid-1;
}
len=max(len,r+1)
q[r+1]=a[i]
}
cout<<len;
最长公共子序列
f[i][j]所有由第一个序列前i个字母和第二个序列前j个字母组成的子序列
a[i],b[j]是否在这个公共子序列中
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
if (a[i] == b[j])f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
}
}
背包问题求方案数
记录下走到每一步的方案,然后求出下一步的maxv,根据maxv的值,来对于当前步数的进行修改,保留
通过路径确定数量
本质就是最短路问题,如果要字典序最小:贪心
for (int i = 0; i < n; i++)
{
int w, v;
cin >> v >> w;
for (int j = m; j >= v; j--)
{
int maxv = max(f[j], f[j - v] + w);
int cnt = 0;
if (maxv == f[j]) cnt += g[j];
if (maxv == f[j - v] + w)cnt += g[j - v];
g[j] = cnt % mod;
f[j] = maxv;
}
}
int res = 0;
for (int i = 0; i <= m; i++)res = max(res, f[i]);
int cnt = 0;
for (int i = 0; i <= m; i++)
if (res == f[i])
cnt = (cnt + g[i]) % mod;
背包问题求具体方案数目
这种就不可以再状态压缩了,所以第一次的两重循环顺序无所谓了
先求出来最大的数目
再从头开始推,看它来自哪里
拦截导弹的贪心
只能拦截比他矮的,问需要几个系统
如果现有子序列的结尾要小于它,就创新的
如果不是就放在后面
每一个存在于这个序列里的数字,就是一个系统,表这个系统的最小的末尾,while找到第一个大于他的系统
for (int i = 0; i < n; i++)
{
int k = 0;
while (k < cnt && g[k] < q[i])k++;
g[k] = q[i];
if (k >= cnt)cnt++;
}
体积的思考(潜水员)
体积最多j:全部初始化为0,v>=0;
体积恰好j:f[0]=0,f[i]=+inf v>=0
体积至少为j:f[0]=0,f[i]=+inf (为负是有方案的)
int v1, v2, w;
cin >> v1 >> v2 >> w;
for (int i = n; i >= 0; i--)
{
for (int j = m; j >= 0; j--)
{
f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
}
}
用前面的能不能表达后面的(货币系统)
先输入然后排序,从小到大
f[0]=1
从小到大,列举各个体积,已有了a[i],则f[j]+=f[j-a[i]](这个j除了a的其他可以被表示就可以被表示)
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
sort(a, a + n);
int m = a[n - 1];
memset(f, 0, sizeof f);
f[0] = 1;
int res = 0;
for (int i = 0; i < n; i++)
{
if (!f[a[i]])res++;
for (int j = a[i]; j <= m; j++)f[j] += f[j - a[i]];
}
cout << res << endl;
有依赖的背包
就是一棵树,儿子结点依赖于根节点,dfs遍历到叶结点,然后回溯
总体思路还是三重循环,循环物品(不同的子结点),循环体积,循环决策(儿子有多少)
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i])
{
int son = e[i];
dfs(e[i]);
//分组背包
for (int j = m - v[u]; j >= 0; j--)//循环体积
{
for (int k = 0; k <= j; k++)//循环决策
{
f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
}
}
}
//将物品u加进去
for (int i = m; i >= v[u]; i--)f[u][i] = f[u][i - v[u]] + w[u];
for (int i = 0; i < v[u]; i++)f[u][i] = 0;
}
dfs(root)
cout<<<f[root][m]<<endl;
状态机(叽叽叽)
多开一维数组来表示状态
每一次循环里面,对于两种状态进行取值
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
f[i][j][0] = max(f[i - 1][j][0], f[i - 1][j][1] + w[i]);
f[i][j][1] = max(f[i - 1][j][1], f[i - 1][j - 1][0] - w[i]);
}
}
状态压缩DP
细讲棋盘式
对于每一排存储成一个二进制数,表示这个坑填还是不填(用于某些坑要跳过的题目筛查时用),对于每一种排列(0-1<<m-1)进行check,并把满足条件的保存到state数组内
两次循环可能的来进行关系的判断,满足的可以添加到以a为头的可能数组中
不能有连续两个相邻state>>1&1&&state>>i+1&1
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int g[N];
vector<int>state;
vector<int>head[M];
int f[N][M];
bool check(int state)
{
for (int i = 0; i < m; i++)
{
if ((state >> 1 & 1) && (state >> i + 1 & 1))
return false;
}
return true;
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < m; j++)
{
int t;
cin >> t;
g[i] += !t << j;
}
}
for (int i = 0; i < 1 << m; i++)
{
if (check(i))
state.push_back(i);
}
for (int i = 0; i < state.size(); i++)
{
for (int j = 0; j < state.size(); j++)
{
int a = state[i], b = state[b];
if ((a & b) == 0)
head[i].push_back(j);
}
}
f[0][0] = 1;
for (int i = 1; i <= n + 1; i++)
{
for (int a = 0; a < state.size(); a++)
{
for (int b : head[a])
{
if (g[i] & state[a])continue;
f[i][a] = (f[i][a] + f[i - 1][b]) % mod;
}
}
}
cout << f[n + 1][0] << endl;
return 0;
}
树的直径
任取一个点作为起点,寻找距离该点最远的一点,再找距离它最远的一个点,两点间距离就是直径,要维护最长和次长
主要利用深搜,返回是到最下面叶结点的距离
int dfs(int u,int father)
{ int dist=0;
int d1=0,d2=0;
for(int i=h[u];~i;i=ne[i])
{ int j=e[i];
if(j==father)continue;
int d=dfs(j,u)+w[i];
dist=max(dist,d);
if (dist >= d1)d2 = d1, d1 = dist;
else if (dist > d2)d2 = dist;
}
ans = max(ans, d1 + d2);
return dist;
树形dp
往往是dfs
dfs(1,-1)
先深搜,在回溯,根据回溯的进行dp
思考根和节点之间关系 寻找u和j之间关系
void dfs(int u)
{
f[u][0] = 0;
f[u][1] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += f[j][1];
f[u][1] += min(f[j][0], f[j][1]);
}
}
有些状态复杂,但可以通过其他状态推的,先把简单的找出来,再通过一次把复杂的需要有处理结果支持的处理出来
重点是分析处理出来父亲节点状态和儿子节点状态的关系
void dfs(int u)
{
f[u][2] = w[u];
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0]+= min(f[j][1], f[j][2]);
f[u][2] += min(min(f[j][0], f[j][1]), f[j][2]);
}
f[u][1] = -1e9;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
f[u][1] = min(f[u][1], f[j][2] + f[u][0] - min(f[j][1], f[j][2]));
}
}
数位dp
求区间看成前缀和之差
f[第几位][最高的数字/上一位留下的需要满足的东西]
把每一位数字拆分开1存入数组,从最高位开始循环,res+满足的处理好的dp数量
把这个数但单独提出来,循环比他小的
分为1-这个数小1,和取这个数位本身的数
最后处理这个数字本身算不算答案
初始化处理的时候只有1位的答案要单独赋值
不要62
void init()
{
for (int i = 0; i <= 9; i++)
{
if (i != 4)
f[1][i] = 1;
}
for(int i=2;i<N;i++)
for (int j = 0; j <= 9; j++)
{
if (j == 4)continue;
for (int k = 0; k <= 9; k++)
{
if (k == 4 || j == 6 && k == 2)continue;
f[i][j] += f[i - 1][k];
}
}
}
int dp(int n)
{
if (!n)return 1;
vector<int>nums;
while (n)nums.push_back(n % 10),n /= 10;
int res = 0;
int last = 0;
for (int i = nums.size() - 1; i >= 0; i--)
{
int x = nums[i];
for (int j = 0; j < x; j++)
{
if (j == 4 || last == 6 && j == 2)continue;
res += f[i + 1][j];
}
if (x == 4 || last == 6 && x == 2)break;
last = x;
if (!i)res++;
}
}
不降数
void init()
{
for (int i = 0; i <= 9; i++)f[1][i] = 1;
for (int i = 2; i < N; i++)
for (int j = 0; j <= 9; j++)
for (int k = j; k <= 9; k++)
f[i][j] += f[i - 1][k];
}
int dp(int n)
{
if (!n)return 1;
vector<int>nums;
while (n)nums.push_back(n % 10), n /= 10;
int res = 0;
int last = 0;
for (int i = nums.size() - 1; i >= 0; i--)
{
int x = nums[i];
for (int j = last;j < x; j++)
{
res += f[i + 1][j];
}
if (x < last) break;
last = x;
if (!i)res++;
}
return res;
}
单调队列优化DP
举个例子:某两个城市之间有n个烽火台,连续m个中至少有一个要放出信号,求总代价
f[i]表示点燃第i个的合理最少数量 f[i]=min(f[j])+wi
所以要求在区间长度为m中的最小值
循环 每一次先判断头部出没出队,更新最新头部,得出f[i],再将这个新的值加入到队列中
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> w[i];
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)
{
if (q[hh] < i - m)hh++;
f[i] = f[q[hh]] + w[i];
while (hh <= tt && f[q[tt]] >= f[i])tt--;
q[++tt] = i;
}
int res = 1e9;
for (int i = n - m + 1; i <= n; i++)res = min(res, f[i]);
cout << res;
return 0;
}
最大子序列和
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> s[i];
s[i] += s[i - 1];
}
int res = 0;
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)
{
if (q[hh] < i - m)hh++;
res = max(res, s[i] - s[q[hh]]);
while (hh << tt && s[q[tt]] >= s[i])tt--;//找小的捏
q[++tt] = i;
}
cout << res;
return 0;
}
FLOOD FILL
没有什么好说的其实就是设置st,队列,方向变量
放一道城堡问题
#include<iostream>
#include<cstring>
#include<algorithm>
#define x first
#define y second
using namespace std;
typedef pair<int, int>PII;
const int N = 55, M = N * N;
int n, m;
int g[N][N];
PII q[M];
bool st[N][N];
int bfs(int sx, int sy)
{
int dx[4] = { 0,-1,0,-1 }, dy[4] = { -1,0,1,0 };
int hh = 0, tt = 0;
int area = 0;
q[0] = { sx,sy };
st[sx][sy] = true;
while (hh <= tt)
{
PII t = q[hh++];
area++;
for (int i = 0; i < 4; i++)
{
int a = t.x + dx[i], b = t.y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= m)continue;
if (st[a][b])continue;
if (g[t.x][t.y] >> 1 & 1)continue;
q[++tt] = { a,b };
st[a][b] = true;
}
}
return area;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
int cnt = 0, area = 0;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if (!st[i][j])
{
area = max(area, bfs(i, j));
cnt++;
}
cout << cnt << endl;
cout << area << endl;
return 0;
}
多元BFS
具有两段性,具有单调性
魔板:广搜输入start,有三种不同的string,对此循环,如果从来没有走过这种这种状态,就把走到这种的距离增加1,并且加入队列
/*在成功地发明了魔方之后,拉比克先生发明了它的二维版本,称作魔板。这是一张有8个大小相同的格子的魔板:
1 2 3 4
8 7 6 5
我们知道魔板的每一个方格都有一种颜色。
这8种颜色用前8个正整数来表示。
可以用颜色的序列来表示一种魔板状态,
规定从魔板的左上角开始,沿顺时针方向依次取出整数,
构成一个颜色序列。对于上图的魔板状态,
我们用序列(1,2,3,4,5,6,7,8)来表示。这是基本状态。
这里提供三种基本操作,分别用大写字母“A”,“B”,“C”来表示
(可以通过这些操作改变魔板的状态):
“A”:交换上下两行;
“B”:将最右边的一列插入最左边;
“C”:魔板中央四格作顺时针旋转。
下面是对基本状态进行操作的示范:
A: 8 7 6 5
1 2 3 4
B: 4 1 2 3
5 8 7 6
C: 1 7 2 4
8 6 3 5
对于每种可能的状态,这三种基本操作都可以使用。
你要编程计算用最少的基本操作完成基本状态到目标状态的转换,输出基本操作序列。
Input*/
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#include<queue>
using namespace std;
char g[2][4];
unordered_map<string, int>dist;
unordered_map<string, pair<char, string> >pre;
queue<string>q;
void set(string state)
{
for (int i = 0; i < 4; i++)g[0][i] = state[i];
for (int i = 3, j = 4; i >= 0; i--, j++)g[1][i] = state[j];
}
string get()
{
string res;
for (int i = 0; i < 4; i++)res += g[0][i];
for (int i = 3; i >= 0; i--)res += g[1][i];
return res;
}
string move0(string state)
{
for (int i = 0; i < 4; i++)g[0][i] = state[i];
for (int i = 3, j = 4; i >= 0; i--, j++)g[1][i] = state[i];
}
string move1(string state)
{
set(state);
char v0 = g[0][3], v1 = g[1][3];
for (int i = 3; i > 0; i--)
for (int j = 0; j < 2; j++)
g[j][i] = g[j][i - 1];
g[0][0] = v0, g[1][0] = v1;
return get();
}
string move2(string state)
{
set(state);
char v = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = v;
return get();
}
void bfs(string start, string end)
{
if (start == end)return;
q.push(start);
dist[start] = 0;
while (q.size())
{
auto t = q.front();
q.pop();
string m[3];
m[0] = move0(t);
m[1] = move1(t);
m[2] = move2(t);
for (int i = 0; i < 3; i++)
{
string str = m[i];
if (dist.count(str) == 0)
{
dist[str] = dist[t] + 1;
pre[str] = { char(i + 'A'),t };
if (str == end)break;
q.push(str);
}
}
}
}
int main()
{
int x;
string start, end;
for (int i = 0; i < 8; i++)
{
cin >> x;
end += char(x + '0');
}
for (int i = 0; i < 8; i++)start += char(i + '1');
bfs(start, end);
cout << dist[end] << endl;
string res;
while (end != start)
{
res += pre[end].first;
end = pre[end].second;
}
reverse(res.begin(), res.end());
if (res.size())cout << res << endl;
return 0;
}
双向广搜(解决最小步数)
电路维修
两位坐标存pair
用双端队列
对于可以一步到达的d=dist,需要转一下的d=dist+1,并且存到队尾
0,1的广搜都可以用双端队列
#include<cstring>
#include<iostream>
#include<algorithm>
#include<deque>
#define x first
#define y second
using namespace std;
typedef pair<int, int>PII;
const int N = 510, M = N * N;
int n, m;
char g[N][N];
int dist[N][N];
bool st[N][N];
int bfs()
{
deque<PII>q;
memset(st, 0, sizeof st);
memset(dist, 0x3f, sizeof dist);
char cs[5] = "\\/\\/";
int dx[4] = { -1,-1,1,1 }, dy[4] = { -1,1,1,-1 };
int ix[4] = { -1,-1,0,0 }, iy[4] = { -1,0,0,-1 };
dist[0][0] = 0;
q.push_back({ 0,0 });
while (q.size())
{
auto t = q.front();
q.pop_front();
int x = t.x, y = t.y;
if (x == n && y == m)return dist[x][y];
if (st[x][y])continue;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a<0 || a>n || b<0 || b>m)continue;
int ga = x + ix[i], gb = y + iy[i];
int w = (g[ga][gb] != cs[i]);//相等就是0
int d = dist[x][y] + w;
if (d <= dist[a][b])
{
dist[a][b] = d;
if (!w)q.push_front({ a,b });
else q.push_back({ a,b });
}
}
}
return -1;
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n >> m;
for (int i = 0; i < n; i++)cin >> g[i];
if (n + m & 1)puts("NO SOLUTION");
else cout << bfs();
}
return 0;
}
A*算法
估计距离必须大于真实距离
bfs入队判重,digkstra出队判重,A*不能判重
一个unordered_map存状态string对应的距离
优先队列里面存估计距离和状态(根据估计距离的大小来判断先走哪一个状态)
while中利用这个状态,找到x,进行方向上的转换,如果这种状态再dist中没有,或者距离更短,则存进来,(dist和heap(heap里面存的是现在的距离和估值距离)
#include<cstring>
#include<iostream>
#include<algorithm>
#include<unordered_map>
#include<queue>
#define x fisrt
#define y second
using namespace std;
typedef pair<int, string> PIS;
int f(string state)
{
int res = 0;
for (int i = 0; i < state.size(); i++)
if (state[i] != 'x')
{
int t = state[i] -'1';
res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
return res;
}
string bfs(string start)
{
string end = "12345678x";
char op[] = "urdl";
unordered_map<string, int> dist;
unordered_map<string, pair<char, string> >prev;
priority_queue<PIS, vector<PIS>, greater<PIS> >heap;
dist[start] = 0;
heap.push({ f(start),start });
int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
while (heap.size())
{
auto t = heap.top();
heap.pop();
string state = t.y;
if (state == end) break;
int x, y;
for(int i=0;i<9;i++)
if (state[i] == 'x')
{
x = i / 3, y = i % 3;
break;
}
string source = state;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= 3 || b<0 || b>=3)continue;
state = source;
swap(state[x * 3 + y], state[a * 3 + b]);
if (dist.count(state) == 0 || dist[state] > dist[source] + 1)
{
dist[state] = dist[source] + 1;
prev[state] = { op[i],source };
heap.push({ dist[state] + f(state),state });
}
}
}
string res;
while (end != start)
{
res += prev[end].first;
end = prev[end].y;
}
reverse(res.begin(), res.end());
return res;
}
int main()
{
string start, seq;
char c;
while (cin >> c)
{
start += c;
if (c != 'x')seq += c;
}
int cnt = 0;
for(int i=0;i<8;i++)
for (int j = i; j < 8; j++)
{
if (seq[i] > seq[j])
cnt++;
}
if (cnt & 1)puts("unsolvable");
else cout << bfs(start) << endl;
return 0;
}
第几次弹出就是第几小路
DFS
利用栈来搜索
任意找到一个起点,然后进行深搜,cnt+=dfs(a,b)
相当于先遍历到最后一个点,然后在返回,每一次都把个数+1
例子:红与黑
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 25;
int n, m;
char g[N][N];
bool st[N][N];
int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
int dfs(int x, int y)
{
int cnt = 1;
st[x][y] = true;
for (int i = 0; i < 4; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= n || b < 0 || b >= n)continue;
if (g[a][b] != '.')continue;
if (st[a][b])continue;
cnt += dfs(a, b);
}
return cnt;
}
int main()
{
while (cin >> m >> n, n || m)
{
for (int i = 0; i < n; i++)cin >> g[i];
int x, y;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
if (g[i][j] == '@')
{
x = i;
y = j;
}
}
}
memset(st, 0, sizeof st);
cout << dfs(x, y) << endl;
}
}
用dfs来枚举,就是选择了一种情况,并再次条件下搜索下去
例子:分为互质组
dfs(第几组,当前组内下标,当前多少元素,从哪个开始搜索)
如果可以放进去,组内下标+1,元素+1,哪一个+1
无论如何都可以不放 第几组+1,组内下标=0,当前元素不变,从0开始
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 10;
int n;
int p[N];
int group[N][N];
bool st[N];
int ans = N;
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
bool check(int group[], int gc, int i)
{
for (int j = 0; j < gc; j++)
{
if (gcd(p[group[j]], p[i]) > 1)
return false;
}
return true;
}
void dfs(int g, int gc, int tc, int start)//第几组,当前组内下标,当前多少元素,从何
{
if (g>= ans)return;
if (tc == n)ans = g;
bool flag = true;
for (int i = start; i < n; i++)
if(!st[i]&&check(group[g],gc,i))
{
st[i] = true;
group[g][gc] = i;
dfs(g, gc + 1, tc + 1, i + 1);
st[i] = false;
flag = false;
}
if (flag) dfs(g + 1, 0, tc, 0);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)cin >> p[i];
dfs(1, 0, 0, 0);
cout << ans;
return 0;
}
剪枝大法
- 优化搜索顺序(优先搜索分支小的)
- 排除等效冗余
- 可行性剪枝
- 最优性剪枝
小猫爬山
依次枚举每只小猫(用dfs里的参数),枚举把当前小猫放到某辆车上(循环k)
剪枝
- 优先放重猫,可以减少枚举的数量,把很多从一开 始就不满足放
- 超出重量直接砍
- >=ans
dfs(第几只猫,第几组)
每一次枚举完要还原
void dfs(int u, int k)//the number of cat,the number of cable
{
//最优性
if (k >= ans)return;
if (u == n)
{
ans = k;
return;
}
for (int i = 0; i < k; i++)
{
if (sum[i] + w[u] <= m)
{
sum[i] += w[u];
dfs(u + 1, k);
sum[i] -= w[u];
}
}
sum[k] = w[u];
dfs(u + 1, k + 1);
sum[k] = 0;
}
木棒
长度从小到大枚举
dfs(组数,当前枚举的长度,开始)
- length/sum
- 先枚举长木棍
- 排除冗余
- 组合数枚举(不用管顺序,下标从小到大枚举)
- 当前失败,则相同长度的也一定失败
- 第一个失败当前length失败
- 最后一个失败 当前length失败
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 70;
int n;
int w[N], sum, length;
bool st[N];
bool dfs(int u, int s, int start)//组数,当前长度,开始
{
if (u * length == sum)return true;
if (s == length)return (u + 1, 0, 0);
//i从start枚举
for (int i = start; i < n; i++)
{
if (st[i])continue;
if (s + w[i] > length)continue;
st[i] = true;
if (dfs(u, s + w[i], i + 1))return true;
st[i] = false;
//第一个失败一定搜不到
if (!s)return false;
//最后一个失败这个length一定搜不到
if (s + w[i] == length)return false;
int j = i;
while (j < n && w[j] == w[i])j++;
i = j - 1;
}
return false;
}
int main()
{
while (cin >> n, n)
{
memset(st, 0, sizeof st);
sum = 0;
for (int i = 0; i < n; i++)
{
cin >> w[i];
sum += w[i];
}
sort(w, w + n);
reverse(w, w + n);
length = 1;
while (true)
{
if (sum % length == 0 && dfs(0, 0, 0))
{
cout << length << endl;
break;
}
length++;
if (length > sum)break;
}
}
return 0;
}
生日蛋糕
- 从底向上搜,先r后h
- 预处理出来最小的r和h,保证v+min(v)<=N,s+min(s)<ans
- r和h枚举的时候是由范围的
- 神奇的一个式子
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 25, INF = 1e9;
int n, m;
int minv[N], mins[N];
int R[N], H[N];
int ans = INF;
void dfs(int u, int v, int s)
{
if (v + minv[u] > n)return;
if (s + mins[u] >= ans)return;
if (s + 2 * (n - v) / R[u + 1] >= ans)return;
if (!u)
{
if (v == n)ans = s;
return;
}
for (int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r--)
{
for (int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h--)
{
int t = 0;
if (u == m)t = r * r;
R[u] = r, H[u] = h;
dfs(u - 1, v + r + r * h, s + 2 * r * h + t);
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
minv[i] = minv[i - 1] + i * i * i;
mins[i] = mins[i - 1] + 2 * i * i;
}
R[m + 1] = H[m + 1] = INF;
dfs(m, 0, 0);
cout << ans << endl;
return 0;
}
迭代加深
设置一个搜索上线,到了这个深度,就返回,适用于搜索深度深但是答案在浅层次的
加成序列
对于每个k,都存在两个整数使得 X[k]=X[i]+X[j]
X[1] X[m]=n 求最小的m
剪枝
- 优先枚举较大数
- 排除等效冗余
- 迭代加深
int depth =1;
while(!dfs(1,depth) )depth++;
bool dfs(int u, int depth)
{
if (u > depth)return false;
if (path[u - 1] == n)return true;
bool st[N] = { 0 };
for (int i = u - 1; i >= 0; i--)
{
for (int j = i; j >= 0; j--)
{
int s = path[i] + path[j];
if (s > n || s <= path[u - 1] || st[s]) continue;
st[s] = true;
path[u] = s;
if (dfs(u + 1, depth))return true;
}
}
return false;
}
空间换时间的二分
N个礼物,第i个的重量是G[i],一次可以放入重量之和不超过w的任意多个物品,求一次性能放入的最大重量
如果直接爆搜全部,也就是说直接枚举每个物品放不放到包里,就是2^23
所以可以先搜一半,打表打出来,后来就可以直接查询,在搜后一半然后查出来小于等于w-s的最大值,时间复杂度是2^12.5 + 2^12.5*n/2
dfs1(第几个,重量)
dfs2(第几个,重量)
di'ji'g#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 46;
int n, m, k;
int w[N];
int weights[1 << 25], cnt = 1;
int ans;
void dfs1(int u, int s)//第几个,重量
{
if (u == k)
{
weights[cnt++] = s;
return;
}
dfs1(u + 1, s);//不放
if ((LL)s + w[u] <= m)dfs1(u + 1, s + w[u]);//放
}
void dfs2(int u, int s)
{
if (u >= n)//枚举到这里了
{
int l = 0,r = cnt - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (weights[mid] <= m - s)l = mid;
else r = mid - 1;
}
ans = max(ans, weights[l] + s);
return;
}
dfs2(u + 1, s);
if ((LL)s + w[u] <= m) dfs2(u + 1, s + w[u]);
}
int main()
{
cin >> m >> n;
for (int i = 0; i < n; i++)
cin >> w[i];
sort(w, w + n);
reverse(w, w + n);
k = n / 2 + 2;
dfs1(0, 0);
sort(weights, weights + cnt);
cnt = unique(weights, weights + cnt) - weights;
dfs2(k, 0);
cout << ans << endl;
return 0;
}
IDA*
估价函数<=真实
dfs(当前深度,最高深度)
if depth+f()>max_depth return false;
if f()==0 return true;
举个例子:排书
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 15;
int n;
int q[N];
int w[5][N];
int f()
{
int tot = 0;
for (int i = 0; i + 1 < n; i++)
{
if (q[i + 1] != q[i] + 1)
tot++;
}
return (tot + 2) / 3;//每一次更改三个后继,所以是tot/3上取整
}
bool dfs(int depth, int max_depth)
{
if (depth + f() > max_depth)return false;
if (f() == 0)return true;
for (int len = 2; len <= n; len++)
{
for (int l = 0; l + len - 1 < n; l++)
{
int r = l + len - 1;
for (int k = r + 1; k < n; k++)//向后差在哪里
{
memcpy(w[depth], q, sizeof q);
int y = l;
for (int x = r + 1; x <= k; x++, y++)q[y] = w[depth][x];
for (int x = l; x <= r; x++, y++)q[y] = w[depth][x];
if (dfs(depth + 1, max_depth))return true;
memcpy(q, w[depth], sizeof q);
}
}
}
return false;
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n;
for (int i = 0; i < n; i++)cin >> q[i];
int depth = 0;
while (depth < 5 && !dfs(0, depth))depth++;
if (depth >= 5)puts("5 or more");
else cout << depth << endl;
}
return 0;
}
并查集
一共有两个操纵:
合并两个集合
查询某元素的祖宗节点
可以维护
- 记录每一个集合的的大小(绑定到根节点上)
- 每个点到根节点的距离
举个例子:搭配购买
把搭配的合并成一个物品,只看root点
while (m--)
{
int a, b;
cin >> a >> b;
int pa = find(a), pb = find(b);
if (pa != pb)
{
v[pb] += v[pa];
w[pb] += w[pa];
p[pa] = p[pb];
}
}
for (int i = 1; i <= n; i++)
{
if (p[i] == i)
{
for (int j = vol; j >= v[i]; j--)
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[vol] << endl;
银河英雄传说
维护到根节点距离
当两个集合合并时候,一个到根节点的距离就是size[b],同时size[b]要更新
while (m--)
{
char op[2];
int a, b;
cin >> op >> a >> b;
if (op[0] == 'M')
{
int pa = find(a), pb = find(b);
d[pa] = SIZE[pb];
SIZE[pb] += SIZE[pa];
p[pa] = pb;
}
else
{
int pa = find(a), pb = find(b);
if (pa != pb)puts("-1");
else cout << abs(d[a] - d[b]) - 1<<endl;
}
}
奇数偶数游戏
小 A和小 B在玩一个游戏。首先,小 A
写了一个由 0和 1组成的序列 S,长度为 N。
然后,小 B向小 A提出了 M个问题
在每个问题中,小 B 指定两个数 l和 r,小 A回答 S[l∼r]中有奇数个 1还是偶数个 1。
机智的小 B发现小 A 有可能在撒谎。
例如,小 A曾经回答过 S[1∼3]中有奇数个 1,S[4∼6]中有偶数个 1,现在又回答 S[1∼6]中有偶数个 1,显然这是自相矛盾的。
请你帮助小 B 检查这 M个答案,并指出在至少多少个回答之后可以确定小 A一定在撒谎。
即求出一个最小的 k,使得 01序列 S满足第 1∼k个回答,但不满足第 1∼k+1个回答。
边带权做法
运用前缀和,求出来前面i个数字的1的个数
于是Sr-Sl-1是个奇数就说明了两者的奇数偶数性不同,反之相同
用d[x]表示x与p[x]关系(0表示是同类,1表示不同类)
d[x]+d[y]是奇数说明奇数偶数性不同,反之则相同
if(x,y是同类)
//if(已经在一个树里面)
dx^dy=0无矛盾
//if(不在一棵树上)
dpx=dx^dy ^0 (dx+dpx+dy=0(偶数))
#include <cstdio>
#include <cstring>
#include <iostream>
#include <unordered_map>
using namespace std;
const int N = 2e4 + 10;
int n, m;
int p[N], d[N];
unordered_map<int, int> S;
int get(int x) {
if (S.count(x) == 0) {
S[x] = ++n;
}
return S[x];
}
int find(int x) {
if (p[x] != x) {
int root = find(p[x]);
d[x] ^= d[p[x]];
p[x] = root;
}
return p[x];
}
int main() {
cin >> n >> m;
n = 0;
for (int i = 0; i < N; i++) {
p[i] = i;
}
int res = m; //如果无矛盾, 输出问题数量, 初始的时候为m
for (int i = 1; i <= m; i++) {
int a, b;
string type;
cin >> a >> b >> type;
a = get(a - 1), b = get(b); // s[a-1], s[b]
int t = 0;
if (type == "odd") {
t = 1;
}
int pa = find(a), pb = find(b);
if (pa == pb) {
if ((d[a] ^ d[b]) != t) {
res = i - 1;
break;
}
}
else {
p[pa] = pb;
d[pa] = d[a] ^ d[b] ^ t;
}
}
cout << res << endl;
}
扩展域
把各个条件放到一个集合内,该集合内的一个成立,全部成立
if(x,y是同类)合并x+n,y+n,合并x,y;
反之 反之
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_map>
using namespace std;
const int N = 40010, Base = N / 2;
int n, m;
int p[N];
unordered_map<int, int>S;
int get(int x)
{
if (S.count(x) == 0)S[x] = ++n;
return S[x];
}
int find(int x)
{
if (p[x] != x)p[x] = find(p[x]);
return p[x];
}
int main()
{
cin >> n >> m;
n = 0;
for (int i = 1; i <= m; i++)p[i] = i;
int res = m;
for (int i = 0; i < N; i++)
{
int a, b;
string type;
cin >> a >> b >> type;
a = get(a - 1), b = get(b);
if (type == "even")
{
if (find(a + Base) == find(b))
{
res = i - 1;
break;
}
p[find(a)] = find(b);
p[find(a + Base)] = find(b + Base);
}
else
{
if (find(a) == find(b))
{
res = i - 1;
break;
}
p[find(a + Base)] = find(b);
p[find(a)] = find(b + Base);
}
}
cout << res << endl;
return 0;
}
单源最短路的拓展
虚拟源点
适用于没有起点或者是多个起点的问题
举个例子:聘礼
设置一个虚拟源点,用来表示他自己本身需要的钱,也就是w[0][i]=price
枚举不同的区间,当放松的时候必须满足区间大小才可以,用dijkstra找到达0的最近距离
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int n, m;
int w[N][N], level[N];
int dist[N];
bool st[N];
int dijkstra(int down ,int up)
{
memset(dist, 0x3ff, sizeof dist);
memset(st, 0, sizeof st);
dist[0] = 0;
for (int i = 1; i <= n+1; i++)
{
int t = -1;
for (int j = 0; j <= n; j++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true;
for (int j = 1; j <= n; j++)
if (level[j] >= down && level[j] <= up)
dist[j] = min(dist[j], dist[t] + w[t][j]);
}
return dist[1];
}
int main()
{
cin >> m >> n;
memset(w, 0x3f, sizeof w);
for (int i = 1; i <= n; i++)w[i][i] = 0;
for (int i = 1; i <= n; i++)
{
int price, cnt;
cin >> price >> level[i] >> cnt;
w[0][i] = min(price, w[0][i]);
while (cnt--)
{
int id, cost;
cin >> id >> cost;
w[id][i] = min(w[id][i], cost);
}
}
int res = INF;
for (int i = level[1] - m; i <= level[1]; i++)res = min(res, dijkstra(i, i + m));
cout << res << endl;
return 0;
}
预处理单元最短距离
举个例子:新年好
如果暴力枚举各种走的顺序 超时
先预处理出来每一个走到其他点的最短距离,在枚举从谁出发
深搜d(第几个,当前是,距离)内部循环
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 50010, M = 200010, INF = 0x3f3f3f3f;
int n, m;
int source[6];
int h[N], ne[N], w[N], e[N], idx;
int q[N], dist[6][N];
bool st[N];
void spfa(int start, int dist[])
{
memset(dist, 0x3f, N * 4);
dist[start] = 0;
int hh = 0, tt = 1;
q[0] = start;
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q[tt++] = j;
if (tt == N)tt = 0;
st[j] = true;
}
}
}
}
}
int dfs(int u, int start, int distance)
{
if (u == 6)return distance;//每一次返回
int res = INF;
for (int i = 1; i <= 5; i++)
{
if (!st[i])
{
int next = source[i];
st[i] = true;
res = min(res, dfs(u + 1, i, distance + dist[start][next]));
st[i] = false;
}
}
return res;//终极返回
}
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a];
h[a] = idx++;
}
int main()
{
cin >> n >> m;
source[0] = 1;
for (int i = 1; i <= 5; i++)cin >> source[i];
memset(h, -1, sizeof h);
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < 6; i++)spfa(source[i], dist[i]);
cout << dfs(1, 0, 0);
return 0;
}
与二分结合
举个例子:通信线路
寻找一个费用,最少经过k条大于这个x的边
也就是大于这个费用的边权设为1,其他的为0,如果费用>k,则l=mid
1,0用双端队列(其实就是起到把短的放到前面,heap的意思)
/*在郊区有 N 座通信基站,P 条 双向 电缆,第 i 条电缆连接基站 Ai 和 Bi。
特别地,1 号基站是通信公司的总站,N 号基站位于一座农场中。
现在,农场主希望对通信线路进行升级,其中升级第 i 条电缆需要花费 Li。
电话公司正在举行优惠活动。
农产主可以指定一条从 1 号基站到 N 号基站的路径,并指定路径上不超过 K 条电缆,由电话公司免费提供升级服务。
农场主只需要支付在该路径上剩余的电缆中,升级价格最贵的那条电缆的花费即可。
求至少用多少钱可以完成升级。*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<deque>
using namespace std;
const int N = 1010, M = 20010;
int n, m, k;
int h[N], e[M], ne[M], idx,w[N];
deque<int>q;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(int bound)
{
memset(st, 0, sizeof st);
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
q.push_back(1);
while (q.size())
{
int t = q.front();
q.pop_front();
if (st[t])continue;
st[t] = true;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i], v = w[i] > bound;
if (dist[j] > dist[t] + v)
{
dist[j] = dist[t] + v;
if (!v)q.push_front(j);
else q.push_back(j);
}
}
}
return dist[n] <= k;
}
int main()
{
cin >> n >> m >> k;
memset(h, -1, sizeof h);
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e6 + 1;
while (l < r)
{
int mid = l + r >> 1;
if (check(mid))r = mid;
else l = mid + 1;
}
if (r == 1e6 + 1)r = -1;
cout << r <<endl;
return 0;
}
与dp结合
举个例子:拯救大兵瑞恩
-
如何构建这样一张图?
先用g[i][j]把二维的坐标映射到一维里面
获取那些比较特殊的墙or门,存到edge中
如果是可以走的,就add
再枚举每个点,枚举每个方向,如果不存在edge中,则建立边add(a,b,0) -
如何表示我的状态
用二进制来表示我有钥匙的状态 state|=key -
dp
如果有钥匙d[x,y,state]=d[x,y,state|key]
向上下左右走,无门墙或者有钥匙
d[a,b,state]=min(d[a,b,state],d[x,y,state]+1)
#include<cstring>
#include<iostream>
#include<algorithm>
#include<deque>
#include<set>
#define x first
#define y second
using namespace std;
typedef pair<int, int>PII;
const int N = 11, M = N * N, E = 400, P = 1 << 10;
int n, m, p, k;
int h[N], e[E], w[E], ne[E], idx;
int g[N][N], key[M];
int dist[M][P];
bool st[M][P];
set<PII>edges;
void build()
{
int dx[4] = { -1,0,1,0 };
int dy[4] = { 0,1,0,-1 };
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for (int u = 0; u < 4; u++)
{
int x = i + dx[u], y = j + dy[u];
if (!x || x > n || !y || y > m)continue;
int a = g[i][j], b = g[x][y];
if (edges.count({ a,b }) == 0)add(a, b, 0);
}
}
void add(int a, int b, int c)
{
ne[idx] = h[a];
e[idx] = b;
w[idx] = c;
h[a] = idx++;
}
int bfs()
{
memset(dist, 0x3f, sizeof dist);
dist[1][0] = 0;//坐标 状态
deque<PII>q;
q.push_back({ 1,0 });
while (q.size())
{
PII t = q.front();
q.pop_front();
if (st[t.x][t.y])continue;
st[t.x][t.y] = 1;
if (t.x == n * m)return dist[t.x][t.y];
if (key[t.x])
{
int state = t.y | key[t.x];
if (dist[t.x][state] > dist[t.x][t.y])
{
dist[t.x][state] = dist[t.x][t.y];
q.push_front({ t.x,state });
}
}
for (int i = h[t.x]; ~i; i = ne[i])
{
int j = e[i];
if (w[i] && !(t.y >> w[i] - 1 & 1))continue;
if (dist[j][t.y] > dist[t.x][t.y] + 1)
{
dist[j][t.y] = dist[t.x][t.y] + 1;
q.push_back({ j,t.y });
}
}
}
return -1;
}
int main()
{
cin >> n >> m >> p >> k;
for (int i = 1, t = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
g[i][j] = t++;
memset(h, -1, sizeof h);
while (k--)
{
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
int a = g[x1][y1], b = g[x2][y2];
edges.insert({ a,b }), edges.insert({ b,a });
if (c)add(a, b, c), add(b, a, c);
}
build();
int s;
cin >> s;
while (s--)
{
int x, y, id;
cin >> x >> y >> id;
key[g[x][y]] |= 1 << id - 1;
}
cout << bfs() << endl;
return 0;
}
最短路计数
伸缩的时候如果相同,则相加,如果大,则替代
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010, M = 400010, mod = 10003;
int n, m;
int h[N], e[M], ne[M], idx;
int dist[N], cnt[N];
int q[N];
void bfs()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
cnt[1] = 1;
int hh = 0, tt = 0;
q[0] = 1;
while (hh <= tt)
{
int t = q[hh++];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + 1)
{
dist[j] = dist[t] + 1;
cnt[j] = cnt[t];//缁ф壙
q[++tt] = j;
}
else if (dist[j] == dist[t] + 1)
{
cnt[j] = (cnt[j] + cnt[t]) % mod;//澧炲姞
}
}
}
}
void add(int a, int b)
{
ne[idx] = h[a];
e[idx] = b;
h[a] = idx++;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
bfs();
for (int i = 1; i <= n; i++)cout << cnt[i];
return 0;
}
Floyed
最短路
举个例子:牛的旅行
给牧区牧场添一条路径,两个牧区中各选一个牧区,用一条路径连接起来,使得联通的新牧区有最小直径
ans>=所有连通块1自己的最大直径
经过新边的最长路径
- 用floyed求出任何两点之间的最短距离(联通的)
- 求maxd[i]表示和i连通且和i最远的点的距离(内部最大值)
- 枚举哪两个点的连边
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#define x first
#define y second
using namespace std;
typedef pair<int, int>PII;
const int N = 150;
const double INF = 1e20;
int n;
PII q[N];
char g[N][N];
double d[N][N], maxd[N];
double get_dist(PII a, PII b)
{
double dx = a.x - b.x, dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int main()
{
cin >> n;
for (int i = 0; i < n; i++)cin >> q[i].x >> q[i].y;
for (int i = 0; i < n; i++)cin >> g[i];
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if (i != j)
{
if (g[i][j] == '1')d[i][j] = get_dist(q[i], q[j]);
else d[i][j] = INF;
}
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (d[i][j] < INF)
maxd[i] = max(maxd[i], d[i][j]);
double res1 = 0;
for (int i = 0; i < n; i++)res1 = max(res1, maxd[i]);
double res2 = INF;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (d[i][j] >= INF)
res2 = min(res2, get_dist(q[i], q[j]) + maxd[i] + maxd[j]);
cout << max(res1, res2);
return 0;
}
传递闭包
如果a和b关系确定,b和c确定,那么ac确定
if(d(i,k)&&d(k,j)) d(i,j)=1
举个例子:排序
如果d(i,i)=1矛盾
如果d(i,j),d(j,i)必有一个为1,则唯一确定
否则顺序不一定
/*给定 n 个变量和 m 个不等式。其中 n 小于等于 26,变量分别用前 n 的大写英文字母表示。
不等式之间具有传递性,即若 A>B 且 B>C,则 A>C。
请从前往后遍历每对关系,每次遍历时判断:
如果能够确定全部关系且无矛盾,则结束循环,输出确定的次序;
如果发生矛盾,则结束循环,输出有矛盾;
如果循环结束时没有发生上述两种情况,则输出无定解。*/
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 26;
int n, m;
bool g[N][N], d[N][N];
bool st[N];
void floyd()
{
memcpy(d, g, sizeof d);
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
d[i][j] |= d[i][k] && d[k][j];
/*if(d(i,k)&&d(k,j)
d(i,j)=1*/
}
int check()
{
for (int i = 0; i < n; i++)
if (d[i][i])
return 2;
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++)
if (!d[i][j] && !d[j][i])
return 0;
return 1;
}
int main()
{
while (cin >> n >> m, n || m)
{
memset(g, 0, sizeof g);
int type = 0, t;
for (int i = 1; i <= m; i++)
{
char str[5];
cin >> str;
int a = str[0] - 'A', b = str[2] - 'A';
if (!type)
{
g[a][b] = 1;
floyd();
type = check();
if (type)t = i;
}
}
}
}
求最小环
三重循环中,第一重k的循环后
d[i,j]表示从i-j只经过1-k-1的最短路径
所以i,j小于k的时候 i-k固定,k-j固定 进一步需要让j-i最小并且只经过1-k-1
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int n, m;
int d[N][N], g[N][N];
int pos[N][N];
int path[N], cnt;
void get_path(int i, int j)
{
if (pos[i][j] == 0)return;
int k = pos[i][j];
get_path(i, k);
path[cnt++] = k;
get_path(k, j);
}
int main()
{
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= n; i++)g[i][i] = 0;
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int res = INF;
memcpy(d, g, sizeof d);
for (int k = 1; k <= n; k++)
{
for(int i=1;i<k;i++)
for (int j = i + 1; j < k; j++)
{
if ((long long)d[i][j] + g[j][k] + g[k][i] < res)
{
res = d[i][j] + g[j][k] + g[k][i];
cnt = 0;
path[cnt++] = k;
path[cnt++] = i;
get_path(i, j);
path[cnt++] = j;
}
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if (d[i][j] > d[i][k] + d[k][j])
{
d[i][j] = d[i][k] + d[k][j];
pos[i][j] = k;
}
}
if (res == INF)puts("no");
else
{
for (int i = 0; i < cnt; i++)cout << path[i] << " ";
cout << endl;
}
return 0;
}
与倍增的结合
牛站:很魔幻的一道题目,有一点点bellman的感觉,每次更新的是边,只更新一次,要用temp存起来,理解了很多遍没有理解清楚
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
const int N = 210;
int k, n, m, S, E;
int g[N][N];
int res[N][N];
void mul(int c[][N], int a[][N], int b[][N])
{
static int temp[N][N];
memset(temp, 0x3f, sizeof temp);
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
memcpy(c, temp, sizeof temp);
}
void qmi()
{
memset(res, 0x3f, sizeof res);
for (int i = 1; i <= n; i++)res[i][i] = 0;
while (k)
{
if (k & 1)mul(res, res, g);
mul(g, g, g);
k >>= 1;
}
}
int main()
{
cin >> k >> m >> S >> E;
memset(g, 0x3f, sizeof g);
map<int, int>ids;
if (!ids.count(S))ids[S] = ++n;
if (!ids.count(E))ids[E] = ++n;
S = ids[E], E = ids[E];
while (m--)
{
int a, b, c;
cin >> c >> a >> b;
if (!ids.count(a))ids[a] = ++n;
if (!ids.count(b))ids[b] = ++n;
a = ids[a], b = ids[b];
g[a][b] = g[b][a] = min(g[a][b], c);
}
qmi();
cout << res[S][E] << endl;
}
最小生成树
超级源点
自身的也变成有一条路径,一共有n+1个点
拓展成完全图
新边<=wi 不可能
新边>=wi+1
(sizesize-1)(wi-1)
/*给定一棵 N 个节点的树,要求增加若干条边,
把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。
求增加的边的权值总和最小是多少。*/
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 6010;
int n;
struct Edge
{
int a, b, w;
bool operator< (const Edge& t)const
{
return w < t.w;
}
}e[N];
int p[N], SIZE[N];
int find(int x)
{
if (p[x] != x)p[x] = find(p[x]);
return p[x];
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> n;
for (int i = 0; i < n - 1; i++)
{
int a, b, w;
cin >> a >> b >> w;
e[i] = { a,b,w };
}
sort(e, e + n - 1);
for (int i = 1; i <= n; i++)p[i] = i, SIZE[i] = 1;
int res = 0;
for (int i = 0; i < n - 1; i++)
{
int a = find(e[i].a), b = find(e[i].b), w = e[i].w;
if (a != b)
{
res += (SIZE[a] * SIZE[b] - 1) * (w + 1);
SIZE[b] += SIZE[a];
p[a] = b;
}
}
cout << res << endl;
}
return 0;
}
这是一开始都看成一个独立的点,然后由于是按照排序排好的,就是一个天然的prim,1*1-1=0不会增加权值
次小生成树
- 方法a:先求最小生成树,然后枚举删除最小生成树中的边求解
- 先求最小生成树,依次枚举非树边,将其加入,同时从树中去掉一条边,使其成为一棵树
具体步骤
- 求最小生成树,统计标记每条边是树边还是非数边·,同时把最小生成树给建立出来
- 预处理出来任意两点之间的边权最大值(环中,树上)
- 枚举所有非权边 求sum+w-dist[a][b]
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 510, M = 10010;
int n, m;
struct Edge
{
int a, b, w;
bool f;
bool operator< (const Edge& t)const
{
return w < t.w;
}
}edge[M];
int p[N];
int dist[N][N];
int h[N], e[N * 2], w[N * 2], ne[N * 2], idx;
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx++;
}
int find(int x)
{
if (p[x] != x)p[x] = find(p[x]);
return p[x];
}
void dfs(int u, int fa, int maxd, int d[])
{
d[u] = maxd;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j != fa)
{
dfs(j, u, max(maxd, w[i]), d);
}
}
}
int main()
{
cin >> n >> m;;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++)
{
int a, b, w;
cin >> a >> b >> w;
edge[i] = { a,b,w };
}
sort(edge, edge + m);
for (int i = 1; i <= n; i++)p[i] = i;
LL sum = 0;
for (int i = 0; i < m; i++)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
int pa = find(a), pb = find(b);
if (pa != pb)
{
p[pa] = pb;
sum += w;
add(a, b, w), add(b, a, w);
edge[i].f = true;
}
}
for (int i = 1; i <= n; i++)dfs(i, -1, 0, dist[i]);
LL res = 1e18;
for (int i = 0; i < m; i++)
{
if (!edge[i].f)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
if (w > dist[a][b])
res = min(res, sum + w - dist[a][b]);
}
}
cout << res;
}
SPFA找负环
统计每个点入队次数 入队n次,存在负环
统计当前每个点最短路中包含的边数,边数>=n,存在负环
当所有点的入队次数>2n,则很大可能有负环
01分数规划
比值的最大最小,设置一个mid,如果>0,则是看是否存在正环,存在则满足,反之是负环
正环是最长路,dist要根据我们需要的东西来定
举个例子:观光奶牛
/*01分数规划*/
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010, M = 5010;
int n, m;
int wf[N];
int h[N], e[M], wt[M], ne[M], idx;
double dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, wt[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool check(double mid)
{
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
int hh = 0, tt = 0;
for (int i = 1; i <= n; i++)
{
q[tt++] = i;
st[i] = true;
}
while (hh != tt)
{
int t = q[hh++];
if (hh == N)hh = 0;
st[t] = false;
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (dist[j] < dist[t] + wf[t] - mid * wt[i])
{
dist[j] = dist[t] + wf[t] - mid * wt[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n)return true;
if (!st[j])
{
q[tt++] = j;
if (tt == N)tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++)cin >> wf[i];
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
double l = 0, r = 1010;
while (r - l > 1e-4)
{
double mid = (l + r) / 2;
if (check(mid))l = mid;
else r = mid;
}
cout << r;
return 0;
}
约数差分
c在的地方是起点
求最小值,走最长路,求最大值,走最短路
A=B A>=B A<=B
A=A+1
X>=1 X>=x0+1 x0=0
TIP:如果能确定是存在负环的话,可以用栈(深搜)进行spfa,这样子能更快的找到负环然后返回
举个例子:糖果
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
int n, m;
int h[N], e[M], ne[N], w[M], idx;
LL dist[N];
int q[N], cnt[N];
bool st[N];
bool spfa()
{
int hh = 0, tt = 1;
memset(dist, -0x3f, sizeof dist);
dist[0] = 0;
q[0] = 0;
st[0] = true;
while (hh != tt)
{
int t = q[--tt];//用栈
st[t] = false;
for (int i = h[t]; ~i; 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)return false;
if (!st[j])
{
q[tt++] = j;
st[j] = true;
}
}
}
}
return true;
}
void add(int a, int b, int c)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = c;
h[a] = idx++;
}
int main()
{
cin >> m >> n;
memset(h, -1, sizeof h);
while (m--)
{
int x, a, b;
cin >> 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())puts("-1");
else
{
LL res = 0;
for (int i = 1; i <= n; i++)res += dist[i];
cout << res;
}
return 0;
}
在线求LCA
倍增 fa[i][j]表示从i开始,向上走2^j能够到达的节点
步骤:
- 先让两点跳到同一个高度
- 让他们继续向上跳,直到到达公共的下一层
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
#include<queue>
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e6 + 10;
int e[N], ne[N], depth[N], h[N],q[N];
int fa[N][16];
int idx = 0;
int n;
void bfs(int root)
{
memset(depth, 0x3f, sizeof depth);
depth[root] = 1;
depth[0] = 0;
int hh = 0, tt = 0;
q[0] = root;
while (hh <= tt)
{
int t = q[hh++];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[++tt] = j;
fa[j][0] = t;
for (int k = 1; k<= 15; k++)
fa[j][k] = fa[fa[j][k - 1]][k-1];
}
}
}
}
int lca(int a, int b)
{
if (depth[a] < depth[b])swap(a, b);
for (int k = 15; k >= 0; k--)
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b)return a;
for (int k = 15; k >= 0; k--)
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
void add(int a, int b)
{
ne[idx] = h[a];
e[idx] = b;
h[a] = idx++;
}
int main()
{
cin >> n;
int root = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i++)
{
int a, b;
cin >> a >> b;
if (b == -1)root = a;
else add(a, b), add(b, a);
}
bfs(root);
int m;
cin >> m;
while (m--)
{
int a, b;
cin >> a >> b;
int p = lca(a, b);
if (p == a)puts("1");
else if (p == b)puts("2");
else puts("0");
}
return 0;
}
离线求LCA(TARJAN)
深度优先遍历的时候,将所有的点分为三大类:已经遍历过且回溯过的点,正在搜索的分支,还没有搜索到的点
举个例子:给出n个点的一张图,多次询问两个点之间的距离:d(x)+d(y)-2*d§
dfs设置深度
tarjan:把这个点状态设为1(表示正在走的分支),进行深搜,把所有子结点都进入tarjan,并且把j和u合并,这样之后可以询问了,如果询问的点已经被搜过了,那么就可以寻找其父亲节点(父亲节点必然也是在这个分支上的,大不了就是最上面的根节点)
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
typedef pair<int, int>PII;
const int N = 20010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[N];
int st[N];
vector<PII> query[N];
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
}
void dfs(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa)continue;
dist[j] = dist[u] + w[i];
dfs(j, u);
}
}
int find(int x)
{
if (p[x] != x)p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto item : query[u])
{
int y = item.first, id = item.second;
if (st[y] == 2)
{
int anc = find(y);
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
st[u] = 2;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
if (a != b)
{
query[a].push_back({ b,i });
query[b].push_back({ a,i });
}
}
for (int i = 1; i <= n; i++)p[i] = i;
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; i++)cout << res[i];
return 0;
}
有向图的强连通分量DFS
对于一个有向图连通分量,连通分量里任意两点u,v,可以互相走到
有向图经过缩点可以编成有向无环图
-如何判断是不是在强连通分量中
- 存在后向边指向祖先节点
- 先走横插边,再走到祖先
tarjan的实现:
- 首先设置时间戳
- 加入栈,并且把在栈设置为正确
- 深搜它的子结点,如果它还没有过,则对它进行处理,这个父亲的low进行更新,如果在里面了,则进行更新
- 询问这个是不是我要找的割点,如果是的话,把连通块的数量增加1,对于所有的y,y是栈内所有的,设置为不在站内,并把id设置为scc_cnt,这个连通块size+1,直到到达了y==u
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 10010, M = 50010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, SIZE[N];
int dout[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void tarjan(int u)
{
dfn[u] = low[n] = ++timestamp;
stk[++top] = u, in_stk[u] = true;
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 (in_stk[j])low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++scc_cnt;
int y;
do
{
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
SIZE[scc_cnt]++;
} while (y != u);
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--)
{
int a, b;
cin >> a >> b;
add(a, b);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b)dout[a]++;
}
int zeros = 0, sum = 0;
for(int i=1;i<=scc_cnt;i++)
if (!dout[i])
{
zeros++;
sum += SIZE[i];
if (zeros > 1)
{
sum = 0;
break;
}
}
cout << sum << endl;
}
使其成为强连通分量
- 对于有向图,起点P个,终点Q个,还需要max(p,q)
- 对于无向图 cnt+1/2
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110, M = 10010;
int n;
int h[N], ne[M], e[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;
stk[++top] = u, in_stk[u] = true;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j])
{
low[u] = min(low[u], dfn[j]);
}
}
if (dfn[u] == low[u])
{
++scc_cnt;
int y;
do
{
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
} while (y != u);
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++)
{
int t;
while (cin >> t, t)add(i, t);
}
for (int i = 1; i <= n; i++)
if (!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
for (int j = h[i]; j != -1; j = ne[i])
{
int k = e[j];
int a = id[i], b = id[k];
if (a != b)
{
dout[a]++;
din[b]++;
}
}
int a = 0, b = 0;
for (int i = 1; i <= scc_cnt; i++)
{
if (!din[i])a++;
if (!dout[i])b++;
}
cout << a;
if (scc_cnt == 1)puts("0");
else cout << max(a, b);
}
强连通图解决差束约分问题
就是看是否存在环
tarjan +缩点建图+拓扑排序
原理:
首先用tarjan求scc
一个正环一定是某一个scc当中的,对于一个scc中的所有边,
只要一个边的权重是严格>0
如 u + w → v,w>0
又u和v 在一个scc中,则v也一定能到u(且w[v][u]>=0(因为我们的不等式约束得到的))
即只要scc中有一个边 >=0 就必然存在正环
则 scc中无正环 <=> scc中的 边==0 <=> scc中所有点相同 (由不等式知双向边0时 A=B<=> 可近似看成一个点
那么当没有正环时,经过tarjan后的图就是topo图
x[i]最小 <=> 求最长路dist[i]
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, M = 600010;
int n, m;
int h[N], hs[N], e[M], ne[M], w[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, scc_size[N];
int dist[N];
void add(int h[], int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void tarjan(int u)
{
dfn[u] = low[u] = timestamp++;
stk[++top] = u, in_stk[u] = true;//stk[++top]要和stk[top--]配 而不是stk[top++] stk[0++] = u stk[1++] = t
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
scc_cnt++;
int y;
do
{
y = stk[top--];//忘了强连通分量都是通过dfs后在一个栈里的
in_stk[y] = false;//漏了
id[y] = scc_cnt;
scc_size[scc_cnt]++;
} while (y != u);//漏了
}
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
memset(hs, -1, sizeof h);
// 超级源点 和i有一个1的边
for (int i = 1; i <= n; i++)add(h, 0, i, 1);
while (m--)
{
int t, a, b;
cin >> t >> a >> b;
if (t == 1)add(h, b, a, 0), add(h, a, b, 0);
else if (t == 2)add(h, a, b, 1);
else if (t == 3)add(h, b, a, 0);
else if (t == 4)add(h, b, a, 1);
else add(h, a, b, 0);
}
tarjan(0);
bool success = true;
for (int i = 0; i <= n; i++)
{
for (int j = h[i]; ~j; j = ne[j])
{
int k = e[j];
int a = id[i], b = id[k];
// 如果a和b在一个scc里,判断w[a][b]是否>0
if (a == b)
{
if (w[j] > 0)
{
success = false;
break;
}
}
// 如果不在一个scc里 在新图里加一条边
else add(hs, a, b, w[j]);
}
if (!success) break;
}
if (!success) cout << "-1";
else
{
// 有解 求最长路
for (int i = scc_cnt; i; i--)
{
for (int j = hs[i]; ~j; j = ne[j])
{
int k = e[j];
dist[k] = max(dist[k], dist[i] + w[j]);
}
}
LL res = 0;
// 结果 = 新图里每个scc的距离 * scc里的点数 = dist[scc] * cnt[scc]
for (int i = 1; i <= scc_cnt; i++) res += (LL)dist[i] * scc_size[i];
cout << res;
}
return 0;
}
二分图
棋盘是天然的二分,所以查找的时候只要找x+y是奇数或是偶数的
染色法
举个例子:关押罪犯
看能否把大于x的哪些人二分到两边
dfs(当前是什么,眼色,mid)
bool dfs(int u, int c, int mid)
{
color[u] = c;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (w[i] <= mid)continue;
if (color[j])
{
if (color[j] == c)return false;
}
else if (!dfs(j,3-c,mid))return false;
}
return true;
}
bool check(int mid)
{
memset(color, 0, sizeof color);
for (int i = 1; i <= n; i++)
if (color[i] == 0)
if (!dfs(i, 1, mid))return false;
return true;
}
int main()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int l = 0, r = 1e9;
while (l < r)
{
int mid = l + r >> 1;
if (check(mid))r = mid;
else l = mid + 1;
}
cout << r << endl;
return 0;
}
匈牙利算法
最大匹配数=最小点覆盖=总点数-最大独立集=总点数-最小路径覆盖
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 110;
int n, m, k;
bool g[N][N], st[N];
int match[N];
bool find(int x)
{
for(int i=1;i<m;i++)
if (!st[i] && g[x][i])
{
st[i] = true;
int t = match[i];
if (t == 0 || find(t))
{
match[i] = x;
return true;
}
}
return false;
}
int main()
{
while (cin >> n, n)
{
cin >> m >> k;
memset(g, 0, sizeof g);
memset(match, 0, sizeof match);
while (k--)
{
int t, a, b;
cin >> t >> a >> b;
if (!a || !b)continue;
g[a][b] = true;
}
int res = 0;
for (int i = 1; i < n; i++)
{
memset(st, 0, sizeof st);
if (find(i))res++;
}
cout << res;
}
}
最大独立集
选出最多点,使得选出来的点之间没有边
二分图中,求最大独立集
《=》去掉最少的点,将所有边破坏
《=》找最小点覆盖
《=》找最大匹配
举个例子:骑士
选点:选的点两两之间没有边
n*m-k-最大匹配数
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
#define x first
#define y second
typedef pair<int, int>PII;
const int N = 110;
int n, m, k;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[] = { -2,-1,1,2,2,1,-1,-2 };
int dy[] = { 1,2,2,1,-1,-2,-2,-1 };
bool find(int x, int y)
{
for (int i = 0; i < 8; i++)
{
int a = x + dx[i], b = y + dy[i];
if (a<1 || a>n || b<1 || b>m)continue;
if (g[a][b] || st[a][b])continue;
st[a][b] = true;
PII t = match[a][b];
if (t.x == 0 || find(t.x, t.y));
{
match[a][b] = { x,y };
return true;
}
}
return false;
}
int main()
{
cin >> n >> m >> k;
for (int i = 0; i < k; i++)
{
int x, y;
cin >> x >> y;
g[x][y] = true;
}
int res = 0;
for(int i=1;i<=n;i++)
for (int j = 1; j <= m; j++)
{
if ((i + j) % 2 || g[i][j])continue;
memset(st, 0, sizeof st);
if (find(i, j))res++;
}
cout << n * m - k - res;
}
}
欧拉回路
起点和终点:奇书
中间点:偶数
对于有向图:
- 所有边连通,所有点的出度等于入度
- 除两点外,所有点的出度等于入度,剩余两点满足,一个出比入多一,一个入比出多一
对于无向图:
- 度数为奇数的点只有0/2个
定一张图,请你找出欧拉回路,
即在图中找一个环使得每条边都在环上出现恰好一次。
输入格式
第一行包含一个整数 t,t∈{1,2},
如果 t=1,表示所给图为无向图,如果 t=2,表示所给图为有向图。
第二行包含两个整数 n,m,表示图的结点数和边数。
接下来 m 行中,第 i 行两个整数 vi,ui,
表示第 i 条边(从 1 开始编号)。
如果 t=1 则表示 vi 到 ui 有一条无向边。
如果 t=2 则表示 vi 到 ui 有一条有向边。
图中可能有重边也可能有自环。点的编号从 1 到 n
统计出度入度,进行第一次判断
在进行dfs,通过边数判断能否联通
dfs,利用栈搜索先到达终点,记录已经走过的点,在回溯的时候走完没有走完的点
for 点边,标记,深搜儿子结点
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 100010, M = 400010;
int type;
int n, m;
int h[N], e[M], ne[M], idx;
bool used[M];
int ans[M / 2], cnt;
int din[N], dout[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs(int u)
{
for (int i = h[u]; ~i; i = ne[i])
{
if (used[i])continue;
used[i] = true;
if (type == 1)used[i ^ 1] = true;
dfs(e[i]);
if (type == 1)
{
int t = i / 2 + 1;
if (i & 1)t = -t;
ans[++cnt] = t;
}
else ans[++cnt] = i + 1;
}
}
int main()
{
cin >> type >> n >> m;
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
add(a, b);
if (type == 1)add(b, a);
din[b]++, dout[a]++;
}
if (type == 1)
{
for(int i=1;i<=n;i++)
if (din[i] + dout[i] & 1)
{
puts("NO");
return 0;
}
}
else
{
for(int i=1;i<=n;i++)
if (din[i] != dout[i])
{
puts("no");
return 0;
}
}
for(int i=1;i<=n;i++)
if (h[i] != -1)
{
dfs(i);
break;
}
if (cnt < m)
{
puts("no");
return 0;
}
puts("yes");
for (int i = cnt; i; i--)cout << ans[i];
}
TIP
如果想要字典序最小,对于每个父亲,有着很多儿子,用vector存,并对其进行排序,从小到大,再深搜
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<stack>
#include<vector>
using namespace std;
const int MAX = 100010;
int n, m, u, v, del[MAX];
int du[MAX][2];
stack <int> st;
vector<int>G[MAX];
void dfs(int now)
{
for (int i = del[now]; i < G[now].size(); i = del[i])
{
del[now] = i + 1;
dfs(G[now][i]);
}
st.push(now);
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
cin >> u >> v;
G[u].push_back(v);
du[u][1]++;
du[v][0]++;
}
for (int i = 1; i <= n; i++)
{
sort(G[i].begin(), G[i].end());
}
int S = 1,cnt[2] = { 0,0 };
bool flag = 1;
for (int i = 1; i <= n; i++)
{
if (du[i][1] != du[i][0]) flag = 0;
if (du[i][1] - du[i][0] == 1) cnt[1]++, S = i;
if (du[i][0] - du[i][1] == 1) cnt[0]++;
}
if ((!flag) && !(cnt[0] == cnt[1] && cnt[0] == 1))
{
printf("No");
return 0;
}
dfs(S);
while (!st.empty())
{
cout << st.top();
st.pop();
}
}
拓扑排序
约束差分求最长路
- 边权没有限制 spfa mn
- 边权非负 tarjan n+m
- 边权为正 拓扑 n+m
一条单向的铁路线上,依次有编号为 1, 2, …, n的 n个火车站。每个火车站都有一个级别,最低为 1级。
现有若干趟车次在这条线路上行驶,每一趟都满足如下要求:如果这趟车次停靠了火车站 x
,则始发站、终点站之间所有级别大于等于火车站 x 的都必须停靠。(注意:起始站和终点站自然也算作事先已知需要停靠的站点)
第一行包含 2个正整数 n,m,用一个空格隔开。
第 i+1 行(1≤i≤m)中,首先是一个正整数 2≤si≤n),表示第 i趟车次有 si个停靠站;接下来有 si个正整数,表示所有停靠站的编号,从小到大排列。
题目给出了停靠站的编号,可以得到起点终点,以及没有停靠点的编号,由题意可以知道,停靠点的等级>=未停靠点等级+1,由此可以进行连边
观察数据,最坏情况1000趟火车,每趟500个点停,500不停,不停的站,向停的连边,5005001000=2.5*10^8,spfa可能超时
由于本题中的所有点的权值都是大于0,并且一定满足要求=>所有车站都等级森严=>
不存在环=>=>可以拓扑排序得到拓扑图使用递推求解差分约束问题
思路是:通过拓扑排序得到拓扑图,跑最长路
tip:如果暴力建图需要mn,可能超时,可以在中间连一个虚拟节点,其向左连0,向右连1,因此需要(m+n)条边,n+m个点
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int N = 2007, M = 5000007;
int n, m;
int ver[M], nex[M], edge[M], head[N], tot;
int din[N];
int vis[N];
int q[N];
int dist[N];
void add(int x, int y, int z) {
ver[tot] = y;
edge[tot] = z;
nex[tot] = head[x];
head[x] = tot++;
din[y] ++;
}
void toposort() {
int hh = 0, tt = -1;
for (int i = 1; i <= n + m; ++i) {//一共n + m个点,要遍历所有的点
if (!din[i]) {
q[++tt] = i;
if (tt == N)tt = 0;
}
}
while (hh <= tt) {
int x = q[hh++];
if (hh == N)hh = 0;
for (int i = head[x]; ~i; i = nex[i]) {
int y = ver[i];
if (--din[y] == 0) {
q[++tt] = y;
if (tt == N)tt = 0;
}
}
}
}
int main() {
scanf("%d%d", &n, &m);
memset(head, -1, sizeof head);
for (int i = 1; i <= m; ++i) {
memset(vis, 0, sizeof vis);
int t, stop;
scanf("%d", &t);
int start = n, end = 1;
while (t--) {
scanf("%d", &stop);//输入的是编号
start = min(start, stop);
end = max(end, stop);
vis[stop] = true;//代表该站要停靠.
}
int source = n + i;//n + 1,虚拟点
for (int j = start; j <= end; ++j) {//该线路上的所有经过的站点的编号一定在始发站和终点站之间
if (vis[j])//要停靠,说明是右部点
add(source, j, 1);
else add(j, source, 0);//左部点
}
}
toposort();
for (int i = 1; i <= n; ++i)//A ≥ 1
dist[i] = 1;
for (int i = 0; i < n + m; ++i) {//手写的队列是从0开始的
int x = q[i];
for (int j = head[x]; ~j; j = nex[j]) {
int y = ver[j], z = edge[j];
dist[y] = max(dist[y], dist[x] + z);
}
}
int res = 0;
for (int i = 1; i <= n; ++i)//满足所有约束条件的解中最大值既是题目要求的最高的级别
res = max(res, dist[i]);
printf("%d\n", res);
return 0;
一般用法
按照先后顺序输出,树变成线性
举个例子:家谱树
先把所有出度为0的进入队伍,寻找儿子,并且将儿子的出度-1,看是否有0的
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N = 110, M = N * N / 2;
int n;
int h[N], ne[N], e[M], idx;
int q[N];
int d[N];
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i++)
if (!d[i])
q[++tt] = i;
while (hh <= tt)
{
int t = q[hh++];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (--d[j] == 0)
q[++tt] = j;
}
}
}
int main()
{
cin >> n;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; i++)
{
int son;
while (cin >> son, son)
{
add(i, son);
d[son]++;
}
}
topsort();
for (int i = 0; i < n; i++)cout << q[i];
return 0;
}
树状数组
兼备快速求前缀和,快速修改某个数的功能
lowbit操作找出最低位的1
add 从这个数向大循环,循环到的+c
sum 从这个数向小循环,res+tr[i]
举个例子:楼兰图腾
寻找数列里面,左边大于(小于)他的,右边大于(小于)它的
每次一个数字,在他的树状数组里就是在它对应的大小的地方+1
先从小到大循环,可以找到每个数左边大于他小于他的个数,也就是寻找树状数组里面已经存在的(已经存在:这些数字是在这个数列里面,是在它的左边)在他右边的数字的个数(这个说明比它大)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N = 200010;
int n;
int a[N];
int tr[N];
int Greater[N], lower[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for (int i = x; i <= n; i += lowbit(i))tr[i] += c;
}
int sum(int x)
{
int res = 0;
for (int i = x; i; i -= lowbit(i))res += tr[i];
return res;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)cin >> a[i];
for (int i = 1; i <= n; i++)
{
int y = a[i];
Greater[i] = sum(n) - sum(y);
lower[i] = sum(y - 1);
add(y, 1);
}
memset(tr, 0, sizeof tr);
LL res1 = 0, res2 = 0;
for (int i = n; i; i--)
{
int y = a[i];
res1 += Greater[i] * (LL)(sum(n) - sum(y));
res2 += lower[i] * (LL)(sum(y - 1));
add(y, 1);
}
cout << res1 << res2;
return 0;
}
举个例子:谜一样的牛
有 n头奶牛,已知它们的身高为 1∼n
且各不相同,但不知道每头奶牛的具体身高。
现在这 n 头奶牛站成一列,已知第 i头牛前面有 Ai 头牛比它低,求每头奶牛的身高。
从后往前去考虑,因为最后一头牛,统计了在它之前的所有的牛,因此加入他比x头牛高,则必然高度是x+1
对于倒数第二头牛来说,是在除去了上述x+1的区间内,选取An-1 +1小的数字
一开始初始化树状数组是说,每个都是1的情况下,t[i]=lowbit[i],因为(l,r]的长度就是r的二进制表示的最后一个1的对应次幂,包含2^i1个数字,也就是x的二进制表示的最后一个1
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 100010;
int n;
int h[N];
int ans[N];
int tr[N];
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for (int i = x; i <= n; i += lowbit(i))tr[i] += c;
}
int sum(int x)
{
int res = 0;
for (int i = x; i; i -= lowbit(i))res += tr[i];
return res;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)cin >> h[i];
for (int i= 1; i <= n; i++)
tr[i] = lowbit(i);
for (int i = n; i; i--)
{
int k = h[i] + 1;
int l = 1, r = n;
while (l < r)
{
int mid = l + r >> 1;
if (sum(mid) >= k)r = mid;
else l = mid + 1;
}
ans[i] = r;
add(r, -1);
}
for (int i = 1; i <= n; i++)cout << ans[i];
}
线段树
动态修改某个数,动态查找某个数
要开4N的空间
pushup:结合题目来把某些数值从下向上传递
build:用递归去建立,直到l==r
query:查询,如果查询区间把某个区间全覆盖了,就返回这个区间,否则继续裂开
modify:直到某个区间的左右和我要的左右完全相同,否则不断裂开,裂完记得pushup
给两种操作。
添加操作:向序列后添加一个数,序列长度变成 n+1;
询问操作:询问这个序列中最后 L 个数中最大的数是多少
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m,p;
struct Node
{
int l, r;
int v;
}tr[N*4];
void pushup(int u)
{
tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}
void build(int u, int l, int r)
{
tr[u] = { l,r };
if (l == r)return;
int mid = l + r >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}
int query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)return tr[u].v;
int mid = tr[u].l + tr[u].r >> 1;
int v = 0;
if (l <= mid)v = query(u << 1, l, r);
if (r > mid)v = max(v, query(u << 1 | 1, l, r));
return v;
}
void modify(int u, int x, int v)
{
if (tr[u].l == x && tr[u].r == x)tr[u].v = v;
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid)modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
}
int main()
{
int n = 0, last = 0;
cin >> m >> p;
build(1, 1, m);
int x;
char op[2];
while (m--)
{
cin >> op >> x;
if (*op == 'Q')
{
last = query(1, n - x + 1, n);
cout << last;
}
else
{
modify(1, n + 1, (last + x) % p);
n++;
}
}
}
举个例子:区间最大公约数
C lrd 把A[l]…A[r]都加上d
Q lr 询问A[l]…A[r]的最大公约数
gcd(a,b)=gcd(a,a-b)
gcd(a,b,c)=gcd(gcd(a,b),gcd(b,c)) =gcd(a,b-a,c-a)
gcd{a1,a2,…,an}=gcd{a1,a2−a1,…,an−an−1}
gcd{a1,a2,…an}=gcd{a1,a2-a1,…,an-an-1}
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 500010;
int n, m;
ll w[N];
struct Node
{
int l, r;
ll sum, d;
}tr[N*4];
ll gcd(ll a, ll b)
{
return b ? gcd(b, a % b) : a;
}
void pushup(Node& u, Node& l, Node& r)
{
u.sum = l.sum + r.sum;
u.d = gcd(l.d, r.d);
}
void pushup(int u)
{
pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}
void build(int u, int l, int r)
{
if (l == r)
{
ll b = w[r] - w[r - 1];
tr[u] = { l,r,b,b };
}
else
{
tr[u].l = l, tr[u].r = 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, ll v)
{
if (tr[u].l == x && tr[u].r == x)
{
ll b = tr[u].sum + v;
tr[u] = { x,x,b,b };
}
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid)modify(u << 1, x, v);
else modify(u << 1 | 1, x, v);
pushup(u);
}
}
Node query(int u, int l, int r)
{
if (tr[u].l >= l && tr[u].r <= r)return tr[u];
else
{
int mid = tr[u].l + tr[u].r >> 1;
if (r <= mid)return query(u << 1, l, r);
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);
Node res;
pushup(res, left, right);
return res;
}
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> w[i];
build(1, 1, n);
int l, r;
ll d;
char op[2];
while (m--)
{
cin >> op >> l >> r;
if (*op == 'Q')
{
auto left = query(1, 1, l), right = query(1, l + 1, r);
cout << abs(gcd(left.sum, right.d));
}
else
{
cin >> d;
modify(1, l, d);
if (r + 1 <= n)modify(1, r + 1, -d);
}
}
}
举个例子:一个简单的整数问题
C lrd 表示把A[l]…A[r]都加上d
C lr 表示询问数列中l-r个数字的和
pushdown:懒标记,用于每一次操作之前,把之前的操作落实到叶结点,以便于其他操作的从后向前的正确性,用于修改操作的时候,减少全部直接到最小区间(modify和query都需要)
线段树维护sum,add,都要在pushdown中进行修改
query:
- (边界条件)如今区间被要查询的全覆盖,则返回sum
- 懒标记下移
- 讨论mid和l,r的关系,进行进一步sum的累加
modify:
- 如果全覆盖,改变sum,区间长度*d,改变add,add+d
- 将懒标记下移,向下递归,pushup
pushdown:
- 改变左右儿子的sum,add,并且把自己的标记清0
pushup:
- 修改之后返回sum
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 100010;
int n, m;
int w[N];
struct Node
{
int l, r;
ll sum, add;
}tr[N*4];
void pushup(int u)
{
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u)
{
auto& root = tr[u], & left = tr[u << 1], & right = tr[u << 1 | 1];
if (root.add)
{
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;
tr[u].add += 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()
{
cin >> n >> m;
for (int i = 1; i <= n; i++)cin >> w[i];
build(1, 1, n);
char op[2];
int l, r, d;
while (m--)
{
cin >> op >> l >> r;
if (*op == 'C')
{
cin >> d;
modify(1, l, r, d);
}
else
cout << query(1, l, r);
}
}
可持久化
前提:本身就是拓扑结构
解决:回溯历史
核心思想:只记录每个版本核上一个版本不同的地方
举个例子:最大异或和
解决异或树:用trie,前缀异或
解决R:用可持久化
解决L:对于要走的子树先判断其max_id=L,再进行递归
insert:
- (前缀下标,处理到第几位,上一个版本,最新版本)
- 如果上一个版本存在,那么我这现在这个版本的左子树就是上个版本的这个树
- 把当前这个新版本开辟出来:idx++
- 继续插入
- 维护max_id
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 600010, M = N * 25;
int n, m;
int s[N];
int tr[M][2], max_id[M];
int root[N], idx;
void insert(int i, int k, int p, int q)
{
if (k < 0)
{
max_id[q] = i;
return;
}
int v = s[i] >> k & 1;
if (p)tr[q][v ^ 1] = tr[p][v ^ 1];
tr[q][v] = ++idx;
insert(i, k - 1, tr[p][v], tr[q][v]);
max_id[q] = max(max_id[tr[q][0], tr[q][1]], max_id[tr[q][1]]);
}
int query(int root,int c,int l)
{
int p = root;
for (int i = 23; i >= 0; i--)
{
int v = c >> i & 1;
if (max_id[tr[p][v ^ 1]] >= l)p = tr[p][v ^ 1];
else p = tr[p][v];
}
return c ^ s[max_id[p]];
}
int main()
{
cin >> n >> m;
max_id[0] = -1;
root[0] = ++idx;
insert(0, 23, 0, root[0]);
for (int i = 1; i <= n; i++)
{
int x;
cin >> x;
s[i] = s[i - 1] ^ x;
root[i] = ++idx;
insert(i, 23, root[i - 1], root[i]);
}
char op[2];
int l, r, x;
while (m--)
{
cin >> op;
if (*op == 'A')
{
cin >> x;
n++;
s[n] = s[n - 1] ^ x;
root[n] = ++idx;
insert(n, 23, root[n - 1], root[n]);
}
else
{
cin >> l >> r >> x;
cout << query(root[r - 1], s[n] ^ x, - 1);
}
}
}
如果你看到这了
那就祝你平安喜乐,腰缠万贯!