图论
拓扑排序
定义
在一张图上,将所有节点排序,使得每个节点的父节点都在其前面出现。
如果图上有环,则这张图没有拓扑序。
模版(BFS)
bool topo(){
queue<int> q;
for (int i=1;i<=n;i++) if (!deg[i]) q.push(i);
while (!q.empty()){
cnt++;
int u=q.front();q.pop();
for (int v:g[u]){
deg[v]--;
if (!deg[v]) q.push(v);
}
}
return (cnt==n);
}
例题
例一 NOIP2020排水系统
不是很板子,主要是分数计算部分需要用到高精度(或是__int128)。
#include<bits/stdc++.h>
using namespace std;
using ll=__int128;
const int maxn=1e5+7;
int deg[maxn],cnt,n,m,id[maxn];
vector<int> g[maxn];
queue<int> q;
ll gcd(ll a,ll b){
if (b==0) return a;
return gcd(b,a%b);
}
ll lcm(ll a,ll b){
return a*b/gcd(a,b);
}
struct water{
ll p,q;
}ans[maxn];
water plu(water a,water b){
water anss;
if (a.q==b.q) {
anss.q=a.q;
anss.p=a.p+b.p;
}else{
ll lc=lcm(a.q,b.q);
anss.p=lc/a.q*a.p+lc/b.q*b.p;
anss.q=lc;
}
return anss;
}
water chu(water a,int b){
ll tmp=gcd(a.p,b);
b/=tmp;a.p/=tmp;
a.q*=b;
return a;
}
void topo(){
for (int i=1;i<=n;i++){
if (!deg[i]) q.push(i);
}
while (!q.empty()){
int u=q.front();q.pop();
if (g[u].empty()) continue;
water tmp={0,1};
if (ans[u].p) tmp=chu(ans[u],g[u].size());
// cerr<<u<<' '<<ans[u].p<<' '<<ans[u].q<<' '<<tmp.p<<' '<<tmp.q<<' '<<g[u].size()<<'\n';
for (int v:g[u]){
// cerr<<v<<'\n';
deg[v]--;
ans[v]=plu(tmp,ans[v]);
// cerr<<v<<' '<<ans[v].p<<' '<<ans[v].q<<'\n';
if (!deg[v]) q.push(v);
}
}
}
void write(ll x){
if (x>9) write(x/10);
putchar('0'+(x%10));
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++){
int d,v;cin>>d;ans[i].q=1,ans[i].p=0;
if (!d) id[++cnt]=i;
while (d--){
cin>>v;
g[i].push_back(v);
deg[v]++;
}
}
for (int i=1;i<=m;i++) ans[i].p=1;
topo();
for (int i=1;i<=cnt;i++){
ll g=gcd(ans[id[i]].p,ans[id[i]].q);
write(ans[id[i]].p/g);putchar(' ');
write(ans[id[i]].q/g);putchar('\n');
// cout<<()<<' '<<(ans[id[i]].q/g)<<'\n';
}
return 0;
}
例二 POI2015 PUS
看起来是差分约束的简化版,可用拓扑排序,但建边的复杂度为 \(O(n^3)\),无法接受。
看到区间,想到线段树,进而想到线段树优化建图(模版题),优化后时间复杂度为 \(O(n\log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int maxn=6e5+7;
using ll=long long;
typedef pair<int,int> PII;
int lson[maxn],rson[maxn],n,m,s,idx,a[maxn],deg[maxn],cnt,dis[maxn],root,id[maxn];
vector<PII> g[maxn];
bool vis[maxn];
void build(int u,int l,int r){
if (l==r){
id[l]=u;idx=max(idx,u);return;
}
int mid=(l+r)>>1;
lson[u]=++idx;rson[u]=++idx;
build(lson[u],l,mid);
build(rson[u],mid+1,r);
g[u].push_back({lson[u],0});deg[lson[u]]++;
g[u].push_back({rson[u],0});deg[rson[u]]++;
}
void update(int p,int pl,int pr,int l,int r,int u){
if (l<=pl&&pr<=r){
g[u].push_back({p,1});deg[p]++;return ;
}
int mid=(pl+pr)>>1;
// cerr<<p<<' '<<mid<<' '<<pl<<' '<<pr<<' '<<l<<' '<<r<<'\n';
if (l<=mid) update(lson[p],pl,mid,l,r,u);
if (r>mid) update(rson[p],mid+1,pr,l,r,u);
}
queue<int> q;
bool topo(){
for (int i=1;i<=idx;i++) if (!deg[i]) q.push(i);
while (!q.empty()){
int u=q.front();q.pop();cnt++;
for (auto p:g[u]){
int v=p.first,w=p.second;
dis[v]=min(dis[v],dis[u]-w);
if (dis[u]-w<a[v]) return 0;
deg[v]--;if (!deg[v]) q.push(v);
}
}
return (cnt==idx);
}
int main(){
scanf("%d%d%d",&n,&s,&m);idx=1;
build(1,1,n);
for (int i=1;i<=s;i++){
int idd;scanf("%d",&idd);scanf("%d",&a[id[idd]]);
dis[id[idd]]=a[id[idd]];
}
for (int i=1;i<=m;i++){
int l,r,k;scanf("%d%d%d",&l,&r,&k);
int pre=l;idx++;
for (int i=1;i<=k;i++){
int x;scanf("%d",&x);
g[id[x]].push_back({idx,0});deg[idx]++;
if (pre<=x-1) update(1,1,n,pre,x-1,idx);
pre=x+1;
}
if (pre<=r) update(1,1,n,pre,r,idx);
}
for (int i=1;i<=idx;i++) if (!dis[i]) dis[i]=1e9;
if (!topo()) {
printf("NIE");return 0;
}
for (int i=1;i<=n;i++) {
if (dis[id[i]]<1){
printf("NIE");return 0;
}
}
printf("TAK\n");
for (int i=1;i<=n;i++) printf("%d ",dis[id[i]]);
return 0;
}
欧拉图
定义
- 欧拉回路:通过图中每条边恰好一次的回路
- 欧拉通路:通过图中每条边恰好一次的通路
- 欧拉图:具有欧拉回路的图
- 半欧拉图:具有欧拉通路但不具有欧拉回路的图
例题
在图中删去一些点,使得图不连通,删去的点在图中不能相邻。
可以考虑在每个连通块内染色,选取每个连通块中总数更少的颜色的点的数量之和作为答案。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+7;
vector<int> g[maxn];
int color[maxn],n,m,deg[maxn],cnt[2];
bool vis[maxn];
bool bfs(int u){
queue<int> q;
q.push(u);color[u]=1;cnt[0]++;
while (!q.empty()){
int u=q.front();q.pop();
for (int v:g[u]){
if (color[v]==color[u]) return 0;
if (!color[v]){
color[v]=3-color[u];
cnt[color[v]-1]++;
q.push(v);
}
}
}
return 1;
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
g[u].push_back(v);g[v].push_back(u);
deg[u]++;deg[v]++;
}
bool f=1;
int ans=0;
for (int i=1;i<=n;i++){
if (!color[i]&°[i]) {
f&=bfs(i);
ans=ans+min(cnt[0],cnt[1]);
cnt[0]=cnt[1]=0;
}
}
if (!f) printf("Impossible");
else {
printf("%d",min(ans,n-ans));
}
return 0;
}
欧拉回路板子题。
先找到奇点,如果没有奇点就默认1为奇点。
从奇点开始DFS,遍历所有与之相邻的点,并删去两点之间的一条边。
在DFS过程中记录答案,最后输出答案即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=507;
int g[maxn][maxn],n,m,node[maxn],deg[maxn],ans[1027],cnt;
bool visnod[maxn];
void dfs(int u){
for (int i=1;i<=n;i++){
int v=node[i];
if (g[u][v]){
g[u][v]--;g[v][u]--;
dfs(v);
}
}
ans[++cnt]=u;
}
int main(){
scanf("%d",&m);
for (int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
g[u][v]++;g[v][u]++;
visnod[u]=visnod[v]=1;
deg[v]++;deg[u]++;
}
for (int i=1;i<=500;i++) if (visnod[i]) node[++n]=i;
int st=node[1];
for (int i=1;i<=n;i++){
if (deg[node[i]]&1){
st=node[i];break;
}
}
dfs(st);
for (int i=cnt;i>0;i--) printf("%d\n",ans[i]);
return 0;
}
欧拉回路拆环。
如果一条边的s==t,则不要这条边,没有意义。
如果有点的度数为奇数,那么肯定无解,输出NIE
在DFS的过程中判断当前元素是否已入栈,如果在就遇到了环,记录答案。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7,maxm=2e6+7;
typedef pair<int,int> PII;
vector<PII> g[maxn];
vector<int> ans[maxn];
stack<int> stk;
int deg[maxn],n,m,edgeid,anscnt;
bool visn[maxn],vise[maxm],ins[maxn];
void dfs(int u){
visn[u]=1;
for (auto p:g[u]){
int v=p.first,id=p.second;
int tmp;
if (id&1) tmp=id+1;
else tmp=id-1;
if (vise[id]) continue;
vise[id]=vise[tmp]=1;
dfs(v);
}
if (!ins[u]){
stk.push(u);ins[u]=1;
}else{
ans[++anscnt].push_back(u);
int t=stk.top();
while (t^u) {
stk.pop();ins[t]=0;ans[anscnt].push_back(t);t=stk.top();
}
ans[anscnt].push_back(u);
}
}
int main(){
ios::sync_with_stdio(0);
cin>>n>>m;
for (int i=1;i<=m;i++){
int u,v,s,t;cin>>u>>v>>s>>t;
if (s^t){
g[u].push_back({v,++edgeid}),g[v].push_back({u,++edgeid});
deg[u]++;deg[v]++;
}
}
for (int i=1;i<=n;i++){
if (deg[i]&1) {
cout<<"NIE";return 0;
}
}
for (int i=1;i<=n;i++){
if (!visn[i]) dfs(i);
}
cout<<anscnt<<'\n';
for (int i=1;i<=anscnt;i++){
cout<<(ans[i].size()-1)<<' ';
for (int u:ans[i]) cout<<u<<' ';
cout<<'\n';
}
return 0;
}
最小生成树
定义
一个无向连通图的最小生成树为这个图中权值最小的生成树。
模版
Kruskal
与并查集联动了
struct edge{
int u,v,w;
bool operator<(const edge &a)const{
return w<a.w;
}
}e[maxm];
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
else return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) return ;
fa[fy]=fx;
}
bool check(int x,int y){
return (fa[find(x)]^fa[find(y)]);
}
int kru(){
int ans=0;
sort(e+1,e+1+m);
for (int i=1;i<=n;i++) fa[i]=i;
for (int i=1;i<=m;i++){
int u=e[i].u,v=e[i].v,w=e[i].w;
if (check(u,v)){
merge(u,v);cnt++;ans+=w;
if (cnt==n) break;
}
}
return ans;
}
Prim
不经常用,但还是贴一个堆优化的。在图为稠密图时,朴素Prim比Kruskal更快。
int prim(){
int ans=0;
memset(dis,0x3f,sizeof dis);
dis[1]=0;
q.push({0,1});
while (!q.empty()){
if (cnt>=n) break;
int u=q.top().second,d=q.top().first;q.pop();
if (vis[u]) continue;
vis[u]=1;
cnt++;ans+=d;
for (auto p:g[u]){
int v=p.first,w=p.second;
if (g[u][v]<dis[v]){
dis[v]=g[u][v];q.push({dis[v],v});
}
}
}
return ans;
}
例题
乍一看好像和最小生成树没关系,但我们可以这么想:把每个矩形区域按墙分为两块并看成点,和与其相连的点建权值为0的边,把墙两边的点建权值为破墙所需代价的边。
要使整个图连通且代价最小,考虑最小生成树,求解即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=507;
char mp[maxn][maxn];
int n,m,w[maxn][maxn],id[maxn][maxn],cnt,fa[maxn*maxn*2],size[maxn*maxn*2];
struct edge{
int u,v,w;
bool operator<(const edge &a)const{
return w<a.w;
}
}e[maxn*maxn*5];
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) return ;
fa[fy]=fx;size[fx]+=size[fy];
}
bool check(int x,int y){
return (fa[find(x)]^fa[find(y)]);
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++){
scanf("%s",mp[i]+1);
}
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++) scanf("%d",&w[i][j]);
}
int tmp=1;
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
id[i][j]=tmp;tmp+=2;
}
}
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
e[++cnt]=(edge){id[i][j],id[i][j]+1,w[i][j]};
}
}
for (int i=1;i<=n;i++){
for (int j=1;j<=m;j++){
if (i>1) e[++cnt]=(edge){id[i][j],id[i-1][j]+1,0};
if (i<n) e[++cnt]=(edge){id[i][j]+1,id[i+1][j],0};
if (j>1) {
int u,v;
if (mp[i][j-1]=='\\') u=id[i][j-1];
else u=id[i][j-1]+1;
if (mp[i][j]=='\\') v=id[i][j]+1;
else v=id[i][j];
e[++cnt]=(edge){u,v,0};
}
if (j<n) {
int u,v;
if (mp[i][j+1]=='\\') u=id[i][j+1]+1;
else u=id[i][j+1];
if (mp[i][j]=='\\') v=id[i][j];
else v=id[i][j]+1;
e[++cnt]=(edge){u,v,0};
}
}
}
sort(e+1,e+1+cnt);
for (int i=1;i<=2*n*m;i++) fa[i]=i,size[i]=1;
int ans=0;
for (int i=1;i<=cnt;i++){
int u=e[i].u,v=e[i].v,w=e[i].w;
if (check(u,v)){
ans+=w;merge(u,v);if (size[find(u)]==2*n*m) break;
}
}
printf("%d",ans);
return 0;
}
为了使能运输的货物最多,可以跑一遍最大生成树,这样使得两点之间路径上边的最小值最大。
由于数据范围较大,所以不能每次询问都去查找答案。
求树上两点之间边的最值,可以用倍增LCA来做。
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int maxn=1e4+7,maxm=5e4+7;
vector<PII> g[maxn];
struct edge{
int u,v,w;
bool operator<(const edge &a)const{
return w>a.w;
}
}e[maxm];
int n,m,q,fa[maxn],size[maxn],cost[maxn][20],f[maxn][20],dep[maxn];
bool vis[maxn];
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
else return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) return ;
fa[fy]=fx;size[fx]+=size[fy];
}
bool check(int x,int y){
return (fa[find(x)]^fa[find(y)]);
}
void kru(){
sort(e+1,e+1+m);
for (int i=1;i<=n;i++) fa[i]=i,size[i]=1;
for (int i=1;i<=m;i++){
int u=e[i].u,v=e[i].v,w=e[i].w;
if (check(u,v)){
merge(u,v);
g[u].push_back({v,w});
g[v].push_back({u,w});
if (size[find(u)]==n) break;
}
}
}
void dfs(int u){
vis[u]=1;
for (auto p:g[u]){
int v=p.first,w=p.second;
if (vis[v]) continue;
dep[v]=dep[u]+1;
f[v][0]=u;cost[v][0]=w;
dfs(v);
}
}
void init(){
for (int i=1;i<=n;i++){
if (!vis[i]){
dep[i]=1;
dfs(i);
f[i][0]=1;
cost[i][0]=INT_MAX;
}
}
for (int i=1;i<=14;i++){
for (int j=1;j<=n;j++){
f[j][i]=f[f[j][i-1]][i-1];
cost[j][i]=min(cost[j][i-1],cost[f[j][i-1]][i-1]);
}
}
}
int lca(int u,int v){
if (find(u)^find(v)) return -1;
int ans=INT_MAX;
if (dep[u]>dep[v]) swap(u,v);
for (int i=14;i>=0;i--){
if (dep[f[v][i]]>=dep[u]){
ans=min(ans,cost[v][i]);
v=f[v][i];
}
}
if (u==v) return ans;
for (int i=14;i>=0;i--){
if (f[u][i]^f[v][i]){
ans=min(ans,min(cost[u][i],cost[v][i]));
u=f[u][i],v=f[v][i];
}
}
ans=min(ans,min(cost[u][0],cost[v][0]));
return ans;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for (int i=1;i<=m;i++){
cin>>e[i].u>>e[i].v>>e[i].w;
}
kru();
init();
cin>>q;
while (q--){
int u,v;cin>>u>>v;
printf("%d\n",lca(u,v));
}
return 0;
}
把地图抽象成一个图,图上的顶点为建筑物,权值为两个建筑物之间的最短距离,就类似上一题了。
在建图的时候使用BFS,得到最短距离。
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int maxn=2007,maxm=5e6+7,maxnn=2002*2002;
const int dx[]={0,0,1,-1},dy[]={1,-1,0,0};
vector<PII> g[maxnn];
struct edge{
int u,v,w;
bool operator<(const edge &a)const{
return w<a.w;
}
}e[maxm];
int n,m,p,T,fa[maxnn],siz[maxnn],cost[maxnn][21],f[maxnn][21],dep[maxnn],cnt;
int id[maxn][maxn],dis[maxn][maxn];
char s[maxn][maxn];
bool vis[maxnn],viss[maxn][maxn];
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
else return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) return ;
fa[fy]=fx;siz[fx]+=siz[fy];
}
bool check(int x,int y){
return (fa[find(x)]^fa[find(y)]);
}
void kru(){
sort(e+1,e+1+cnt);
for (int i=1;i<=p;i++) fa[i]=i,siz[i]=1;
for (int i=1;i<=cnt;i++){
int u=e[i].u,v=e[i].v,w=e[i].w;
if (check(u,v)){
merge(u,v);
g[u].push_back({v,w});
g[v].push_back({u,w});
// if (siz[find(u)]==p) break;
}
}
}
void dfs(int u){
vis[u]=1;
for (auto p:g[u]){
int v=p.first,w=p.second;
if (vis[v]) continue;
dep[v]=dep[u]+1;
f[v][0]=u;cost[v][0]=w;
dfs(v);
}
}
void init(){
for (int i=1;i<=p;i++){
if (!vis[i]){
dep[i]=1;
dfs(i);
f[i][0]=1;
cost[i][0]=0;
}
}
for (int i=1;i<=19;i++){
for (int j=1;j<=p;j++){
f[j][i]=f[f[j][i-1]][i-1];
cost[j][i]=max(cost[j][i-1],cost[f[j][i-1]][i-1]);
}
}
}
int lca(int u,int v){
if (find(u)^find(v)) return -1;
int ans=0;
if (dep[u]>dep[v]) swap(u,v);
for (int i=19;i>=0;i--){
if (dep[f[v][i]]>=dep[u]){
ans=max(ans,cost[v][i]);
v=f[v][i];
}
}
if (u==v) return ans;
for (int i=19;i>=0;i--){
if (f[u][i]^f[v][i]){
ans=max(ans,max(cost[u][i],cost[v][i]));
u=f[u][i],v=f[v][i];
}
}
ans=max(ans,max(cost[u][0],cost[v][0]));
return ans;
}
queue<PII> q;
void bfs(){
while (!q.empty()){
int x=q.front().first,y=q.front().second;q.pop();
viss[x][y]=1;
for (int i=0;i<4;i++){
int fx=x+dx[i],fy=y+dy[i];
if (fx<1||fx>n||fy<1||fy>m||viss[fx][fy]||s[fx][fy]=='#') continue;
if (!id[fx][fy]){
id[fx][fy]=id[x][y];
dis[fx][fy]=dis[x][y]+1;
q.push({fx,fy});
}else if (id[fx][fy]^id[x][y]){
e[++cnt]=(edge){id[x][y],id[fx][fy],dis[fx][fy]+dis[x][y]};
}
}
}
}
int main(){
scanf("%d%d%d%d",&n,&m,&p,&T);
for (int i=1;i<=n;i++){
scanf("%s",s[i]+1);
}
//cerr<<1<<'\n';
for (int i=1;i<=p;i++){
int x,y;scanf("%d%d",&x,&y);
id[x][y]=i;
q.push({x,y});
}
bfs();
//cerr<<2<<'\n';
kru();
//cerr<<3<<'\n';
init();
//cerr<<4<<'\n';
while (T--){
int u,v;scanf("%d%d",&u,&v);
cout<<lca(u,v)<<'\n';
}
//cerr<<5<<'\n';
return 0;
}
图的连通性
相关定义
-
强连通:有向图 G 中任意两个结点连通。
-
强连通分量(Strongly Connected Components,SCC):极大的强连通子图。
-
边双连通:在一张连通的无向图中,对于两个点 \(u\) 和 \(v\) ,无论删去哪条边(只能删去一条)都不能使它们不连通,则 \(u\) 和 \(v\) 边双连通。
-
点双联通:在一张连通的无向图中,对于两个点 \(u\) 和 \(v\),无论删去哪个点(只能删去一个,且不能删 \(u\) 和 \(v\) 自己)都不能使它们不连通,则 \(u\) 和 \(v\) 点双连通。
-
缩点:将强联通分量缩成一个点。
模版
Tarjan
void tarj(int u){
dfn[u]=low[u]=++pos;
instk[u]=1;
stk[++tt]=u;
for (int v:g[u]){
if (!dfn[v]){
tarj(v);
low[u]=min(low[v],low[u]);
}else if (instk[v]) low[u]=min(low[u],dfn[v]);
}
if (dfn[u]==low[u]){
cnt++;
while (stk[tt]^u){
int tmp=stk[tt];tt--;
group[tmp]=cnt;
instk[tmp]=0;
mon[cnt]=min(mon[cnt],a[tmp]);
}
int tmp=stk[tt--];
group[tmp]=cnt;
instk[tmp]=0;
mon[cnt]=min(mon[cnt],a[tmp]);
}
}
缩点
for (int i=1;i<=r;i++){
if (!dfn[p[i]]) tarj(p[i]);
}
for (int u=1;u<=n;u++){
for (int v:g[u]){
if (group[u]^group[v]){
ng[u].push_back(v);
}
}
}
例题
将间谍之间的关系建成图,把告发者指向被告发者。
从可以贿赂的间谍开始跑Tarjan,如果几个间谍形成了一个环,就把他们缩成一个点,点权为几个间谍中最小的点权。
如果在tarjan后还有没有遍历到的点,就判断无解。
最后,将所有入度为0的点的点权相加,得到答案。
#include<bits/stdc++.h>
using namespace std;
const int maxn=3007;
vector<int> g[maxn];
int n,r,m,a[maxn],p[maxn],dfn[maxn],low[maxn],follow[maxn],pos,cnt;
int stk[maxn],tt,mon[maxn],deg[maxn];
bool instk[maxn];
void tarj(int u){
dfn[u]=low[u]=++pos;
instk[u]=1;
stk[++tt]=u;
for (int i=g[u].size()-1;i>=0;i--){
int v=g[u][i];
if (!dfn[v]){
tarj(v);
low[u]=min(low[v],low[u]);
}else if (instk[v]) low[u]=min(low[u],dfn[v]);
}
if (dfn[u]==low[u]){
cnt++;
while (stk[tt]^u){
int tmp=stk[tt];tt--;
follow[tmp]=cnt;
instk[tmp]=0;
mon[cnt]=min(mon[cnt],a[tmp]);
}
int tmp=stk[tt--];
follow[tmp]=cnt;
instk[tmp]=0;
mon[cnt]=min(mon[cnt],a[tmp]);
}
}
int main(){
memset(a,0x3f,sizeof a);
memset(mon,0x3f,sizeof mon);
scanf("%d",&n);
scanf("%d",&r);
for (int i=1;i<=r;i++){
scanf("%d",&p[i]);scanf("%d",&a[p[i]]);
}
scanf("%d",&m);
for (int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
g[u].push_back(v);
}
for (int i=1;i<=r;i++){
if (!dfn[p[i]]) tarj(p[i]);
}
for (int i=1;i<=n;i++){
if (!dfn[i]){
printf("NO\n%d",i);return 0;
}
}
for (int u=1;u<=n;u++){
for (int v:g[u]){
if (follow[u]^follow[v]) deg[follow[v]]++;
}
}
int ans=0;
for (int i=1;i<=cnt;i++){
if (!deg[i]) ans+=mon[i];
}
printf("YES\n%d",ans);
return 0;
}
把ATM机看成点,点权即为ATM机中的钱数。如果几个点形成了一个环,就把这几个点缩成一个点,缩点的点权为几个点的点权和,如果几个点中有酒吧,这个缩点也可以看成酒吧。
最后跑一遍最长路,统计答案即可。
#include<bits/stdc++.h>
using namespace std;
const int maxn=500007;
int dfn[maxn],low[maxn],group[maxn],mon[maxn],mon2[maxn],dp[maxn];
int n,m,st,p,stk[maxn],tt,now,cnt,deg[maxn];
bool instk[maxn],isbar[maxn],bar[maxn];
vector<int> g[maxn],ng[maxn];
void tarj(int u){
dfn[u]=low[u]=++now;
stk[++tt]=u;instk[u]=1;
for (int v:g[u]){
if (!dfn[v]){
tarj(v);
low[u]=min(low[u],low[v]);
}else if (instk[v]) low[u]=min(low[u],dfn[v]);
}
if (dfn[u]==low[u]){
group[u]=++cnt;
while (stk[tt]^u){
int tmp=stk[tt--];
instk[tmp]=0;
group[tmp]=cnt;
mon2[cnt]+=mon[tmp];
bar[cnt]|=isbar[tmp];
}
tt--;
instk[u]=0;
mon2[cnt]+=mon[u];
bar[cnt]|=isbar[u];
}
}
queue<int> q;
void topo(){
q.push(group[st]);
while (!q.empty()){
int u=q.front();q.pop();
// cerr<<"u:"<<u<<' '<<dp[u]<<'\n';
for (int v:ng[u]){
// cerr<<"v:"<<v<<' '<<deg[v]<<'\n';
dp[v]=max(dp[v],dp[u]+mon2[v]);
deg[v]--;if (!deg[v]) q.push(v);
}
}
}
int main(){
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
g[u].push_back(v);
}
for (int i=1;i<=n;i++) scanf("%d",&mon[i]);
scanf("%d%d",&st,&p);
for (int i=1;i<=p;i++){
int tmp;scanf("%d",&tmp);
isbar[tmp]=1;
}
tarj(st);
for (int u=1;u<=n;u++){
if (!dfn[u]) continue;
for (int v:g[u]){
if (group[u]^group[v]){
ng[group[u]].push_back(group[v]);
deg[group[v]]++;
}
}
}
dp[group[st]]=mon2[group[st]];
topo();
int ans=0;
for (int i=1;i<=cnt;i++) if (bar[i]) ans=max(ans,dp[i]);
printf("%d\n",ans);
// for (int i=1;i<=n;i++) cerr<<group[i]<<'\n';
return 0;
}
数据结构
并查集
一种维护元素所属集合的数据结构。
模版
void init(int n){
for (int i=1;i<=n;i++) fa[i]=i;
}
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) continue;
fa[fy]=fx;
}
例题
可以看出行和列的变换是互不干扰的,所以分开来看,最后将两遍的答案相乘。
如果两行可以互换,就把这两行合并到同一个集合中。不难证明,同一个集合中的任意两行都可互换,所以一个集合内的方案数为 \(size!\),最后将所有答案相乘即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
using ll=long long;
const int maxn=57,mod=998244353;
int h[maxn][maxn],n,m,fa[maxn],jc[maxn],size[maxn],ans=1;
void init(){
jc[1]=1;jc[0]=1;
for (int i=2;i<=50;i++) jc[i]=jc[i-1]*i,jc[i]%=mod;
}
int find(int x){
if (fa[x]^x) return fa[x]=find(fa[x]);
else return x;
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if (fx==fy) return;
fa[fx]=fy;
}
signed main(){
init();
scanf("%lld%lld",&n,&m);
for (int i=1;i<=n;i++){
for (int j=1;j<=n;j++){
scanf("%lld",&h[i][j]);
}
}
for (int i=1;i<=n;i++) fa[i]=i;
for (int i=1;i<=n;i++){
for (int j=i+1;j<=n;j++){
bool f=1;
for (int k=1;k<=n&&f;k++){
if (h[i][k]+h[j][k]>m) f=0;
}
if (f) merge(i,j);
}
}
for (int i=1;i<=n;i++) size[find(i)]++;
for (int i=1;i<=n;i++) {
if (fa[i]!=i) continue;
ans*=jc[size[i]];ans%=mod;
}
for (int i=1;i<=n;i++) fa[i]=i,size[i]=0;
for (int i=1;i<=n;i++){
for (int j=i+1;j<=n;j++){
bool f=1;
for (int k=1;k<=n&&f;k++){
if (h[k][i]+h[k][j]>m) f=0;
}
if (f) merge(i,j);
}
}
for (int i=1;i<=n;i++) size[find(i)]++;
for (int i=1;i<=n;i++) {
if (fa[i]!=i) continue;
ans*=jc[size[i]];ans%=mod;
}
printf("%lld",ans%mod);
return 0;
}
单调队列优化DP
介绍
如果当前状态的所有值能从上一个状态的一段中转移,且需要对这一段状态进行RMQ,就可以用单调队列优化。
例题
设 \(dp_{i,j}\) 表示第 \(i\) 天有 \(j\) 张股票时最多能赚的钱数,有4种转移的方式。
1.空手买
\(dp_{i,j}=-ap_i \times j(0\le j\le as_i)\)
2.无操作
\(dp_{i,j}=\max (dp_{i,j},dp_{i-1},j)\)
3.在以前的基础上买股票
\(dp_{i,j}=\max (dp_{i,j},dp_{i-w-1,k}-(j-k)\times ap_i)(j-as_i\le k<j)\)
4.在以前的基础上卖股票
\(dp_{i,j}=\max(dp_{i,j},dp_{i-w-1,k}+(k-j)\times bp_i)(j<k \le j+bs_i)\)
可以看出3、4可以用单调队列优化。
#include<bits/stdc++.h>
using namespace std;
const int maxn=2007;
int n,maxp,w,dp[maxn][maxn],ap[maxn],bp[maxn],as[maxn],bs[maxn];
int q[maxn],hh,tt;
int main(){
memset(dp,-0x3f,sizeof dp);
scanf("%d%d%d",&n,&maxp,&w);
for (int i=1;i<=n;i++){
scanf("%d%d%d%d",&ap[i],&bp[i],&as[i],&bs[i]);
for (int j=0;j<=as[i];j++){
dp[i][j]=j*ap[i]*-1;
}
}
for (int i=1;i<=n;i++){
for (int j=0;j<=maxp;j++){
dp[i][j]=max(dp[i][j],dp[i-1][j]);
}
if (i<=w) continue;
hh=0,tt=-1;
for (int j=0;j<=maxp;j++){
while (hh<=tt&&q[hh]<j-as[i]) hh++;
while (hh<=tt&&dp[i-w-1][q[tt]]+q[tt]*ap[i]<=dp[i-w-1][j]+j*ap[i]) tt--;
q[++tt]=j;
if (hh<=tt) dp[i][j]=max(dp[i][j],dp[i-w-1][q[hh]]+q[hh]*ap[i]-j*ap[i]);
}
hh=0;tt=-1;
for (int j=maxp;j>=0;j--){
while (hh<=tt&&q[hh]>j+bs[i]) hh++;
while (hh<=tt&&dp[i-w-1][q[tt]]+q[tt]*bp[i]<=dp[i-w-1][j]+j*bp[i]) tt--;
q[++tt]=j;
if (hh<=tt) dp[i][j]=max(dp[i][j],dp[i-w-1][q[hh]]+q[hh]*bp[i]-j*bp[i]);
}
}
int ans=0;
for (int i=0;i<=maxp;i++) ans=max(ans,dp[n][i]);
printf("%d",ans);
return 0;
}
最近打atcoder看到一道线段树优化dp,顺便把题放到这儿
线段树
定义
线段树是一种常用来维护区间信息的数据结构,可以在 \(O(log n)\) 的时间复杂度内实现单点修改、区间修改、区间查询等操作。
模版
以下模版维护查询区间和与进行区间加操作。
using ll=long long;
const int maxn=1e5+7;
ll a[maxn],tree[maxn<<2],tag[maxn<<2];
int lson(int x){
return (x<<1);
}
int rson(int x){
return (x<<1|1);
}
void addtag(int p,int l,int r,ll d){
tag[p]+=d;
tree[p]+=(r-l+1)*d;
}
void pushup(int x){
tree[x]=tree[lson(x)]+tree[rson(x)];
}
void pushdown(int p,int l,int r){
if (tag[p]){
int mid=(l+r)>>1;
addtag(lson(p),l,mid,tag[p]);
addtag(rson(p),mid+1,r,tag[p]);
tag[p]=0;
}
}
void build(int p,int l,int r){
if (l==r){
tree[p]=a[l];return ;
}
int mid=(l+r)>>1;
build(lson(p),l,mid);
build(rson(p),mid+1,r);
pushup(p);
}
void update(int p,int pl,int pr,int l,int r,ll d){
if (l<=pl&&pr<=r){
addtag(p,pl,pr,d);return ;
}
pushdown(p,pl,pr);
int mid=(pl+pr)>>1;
if (l<=mid) update(lson(p),pl,mid,l,r,d);
if (r>mid) update(rson(p),mid+1,pr,l,r,d);
pushup(p);
}
ll query(int p,int pl,int pr,int l,int r){
if (l<=pl&&pr<=r) return tree[p];
pushdown(p,pl,pr);
ll res=0;
int mid=(pl+pr)>>1;
if (l<=mid) res+=query(lson(p),pl,mid,l,r);
if (r>mid) res+=query(rson(p),mid+1,pr,l,r);
return res;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>a[i];
build(1,1,n);
while (m--){
int op;cin>>op;
if (op==1){
int l,r;ll k;cin>>l>>r>>k;
update(1,1,n,l,r,k);
}else{
int l,r;cin>>l>>r;
cout<<query(1,1,n,l,r)<<'\n';
}
}
return 0;
}
例题
咕咕咕。
标签:总结,tmp,cnt,int,++,寒假,maxn,ans,集训 From: https://www.cnblogs.com/code953/p/18007034