整理的算法模板合集: ACM模板
题目列表:
题目 | 算法 |
---|---|
A、AcWing 1174. 受欢迎的牛 | 缩点 |
B、AcWing 367. 学校网络 | 缩点 |
C、AcWing 1175. 最大半连通子图 | tarjan缩点 + DP + hash |
D、AcWing 368. 银河 | tarjan缩点 / 差分约束 |
tarjan缩点模板题。
需要注意的是我们缩完点以后,要查找出度和入度的时候应该遍历所有的边,并看这个边的出点和入点来自哪一个缩完点以后的点,这个点的出度或者入度++。或者缩点之后建图,跑新图也可以
#include
#include
#include
#include
#include
using namespace std;
const int N = 50007, M = 500007, INF = 0x3f3f3f3f;
int n,m;
int dfn[N], low[N];
int ins[N];
int ver[M], edge[M], nex[M],head[N], tot;
vector<int>scc[N];
int num;
int stk[N],top;
int scc_cnt;
int c[N];
int out[N];
int scc_num[N];
void add(int x,int y){
ver[tot] = y;
nex[tot] = head[x];
head[x] = tot ++ ;
}
int v[M],h[M],ne[M],tc;
void add_c(int x,int y){
v[tc] = y;
ne[tc] = h[x];
h[x] = tc ++ ;
}
void tarjan(int x){
dfn[x] = low[x] = ++num;
ins[x] = 1;
stk[ ++ top] = x;
for(int i = head[x];~i;i = nex[i]){
int j = ver[i];
if(!dfn[j]){
tarjan(j);
low[x] = min(low[j],low[x]);
}
else if(ins[j]){
low[x] = min(low[x],dfn[j]);
}
}
if(dfn[x] == low[x]){
scc_cnt ++ ;
int y;
do{
y = stk[top -- ];
ins[y] = 0;
c[y] = scc_cnt;
scc[scc_cnt].push_back(y);
scc_num[scc_cnt] ++;
}while(x != y);
}
}
int main(){
memset(head,-1,sizeof head);
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;++i){
int a, b;
scanf("%d%d",&a, &b);
add(a, b);
}
for(int i = 1;i <= n;++i)
if(!dfn[i])
tarjan(i);
for(int x = 1;x <= n;++x){
for(int i = head[x];~i;i = nex[i]){
int y = ver[i];
if(c[x] == c[y])continue;
add_c(c[x],c[y]);
out[c[x]] ++ ;
}
}
int ans = 0;
for(int i = 1;i <= scc_cnt;++i){
if(!out[i]){
if(!ans)
ans = i;
else {puts("0");return 0;}
}
}
printf("%d\n",scc_num[ans]);
return 0;
}
首先,可以进行缩点,这样缩点之后就变成了一个有向无环图(DAG),满足拓扑序。
子任务1:缩点后入度为零的强连通分量必须要有新软件.
子任务2:要求加边后形成一个强连通图。可以考虑到缩点后的DAG上每个点都必须同时具有入度和出度,就可以将没有入度的点的数量记为p,没有出度的点的数量记为q;由于没有出度的点可以直接连接没有入度的点,答案即为max(p,q).
正确性显然.
为了避免混淆,所以我这里缩完点以后就直接建了一个新图,然后之后所有的操作都在新图中进行。
#include
#include
#include
#include
using namespace std;
const int N = 5000007, M = 50000007, INF = 0x3f3f3f3f;
typedef long long ll;
int n, m;
int dfn[N], low[N], num;
int ins[N], stk[N], top;
int head[N], edge[M], nex[N], ver[N], tot;
int hs[N];
int scc_cnt, Size[N], scc_id[N];
int out[N], in[N];
void add(int h[], int x, int y )
{
ver[tot] = y;
nex[tot] = h[x];
h[x] = tot ++ ;
}
void tarjan(int x)
{
dfn[x] = low[x] = ++num;
stk[++ top] = x;
ins[x] = true;
for(int i = head[x];~i ;i = nex[i]){
int y = ver[i];
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
}
else if(ins[y])
low[x] = min(low[x], dfn[y]);
}
if(dfn[x] == low[x]){
++scc_cnt;
int y;
do{
y = stk[top -- ];
ins[y] = false;
scc_id[y] = scc_cnt;
Size[scc_cnt] ++;
}while(x != y);
}
}
int main()
{
memset(head,-1,sizeof head);
memset(hs,-1,sizeof hs);
scanf("%d",&n);
for(int i = 1;i <= n;++i){
int b;
while(~scanf("%d",&b) && b){
add(head,i,b);
}
}
for(int i = 1;i <= n;++i)
if(!dfn[i])
tarjan(i);
for(int x = 1;x <= n;++x)
{
for(int i = head[x];~i;i = nex[i])
{
int y = ver[i];
int a = scc_id[x],b = scc_id[y];
if(a != b){
add(hs,a,b);
}
}
}
for(int x = 1;x <= scc_cnt;++x){
for(int i = hs[x];~i;i = nex[i])
{
int y = ver[i];
out[x] ++ ;
in[y] ++ ;
}
}
int maxo = 0;
int maxi = 0;
for(int x = 1;x <= scc_cnt;++x)
{
if(!out[x])maxo ++ ;
if(!in[x])maxi ++ ;
}
printf("%d\n",maxi);
if(scc_cnt == 1)
puts("0");
else printf("%d\n",max(maxo,maxi));
return 0;
}
这时yxc上课时讲解的截图。
一般用到tarjan算法的题目步骤都非常相似:
导出子图:点是原图的子集,边一定包含与子图中所有点有关的边。
半联通子图:所有的两个点,我要么可以直接一条路过去,要么你可以直接过来。
显然,对于任意一个强连通分量 S ∈ G S\in G S∈G,它一定一个半连通子图,于是我们可以考虑先缩点。缩点之后,原图变成了一个有向无环图,显然,对于任意一条DAG中的一条单向到达的链,它都是一个半连通子图。
所以答案K就是DAG中最大链的顶点个数,答案C就是DAG中不同的最大链有多少种。
由于整个DAG按顺序已经是一个拓扑序了,所以我们可以直接递推DP线性 O ( n ) O(n) O(n)求答案。 设size(u)表示顶点u代表的强连通分量
S中的顶点个数,f(u)表示从顶点u开始延伸的最大链的顶点个数,g(u)表示从顶点u开始延伸的不同的最大链有多少个,则有:
f ( u ) = s i z e ( u ) + m a x ( f ( v ) ) ( u → v ) f(u)=size(u)+max(f(v))\ \ \ \ (u\to v) f(u)=size(u)+max(f(v)) (u→v)
g ( u ) = ∑ g ( v ) ( u → v , s i z e ( u ) + f ( v ) = f ( u ) ) g(u)=∑g(v)(u→v,size(u)+f(v)=f(u)) g(u)=∑g(v)(u→v,size(u)+f(v)=f(u))每次从一个没有被访问过的顶点开始做深度优先搜索,回溯时计算每个顶点的f,g值。K,C也可以按照相同的方式进行更新。
本题需要判重去重,因为第二问是总的方案数,如果边是一摸一样的那么方案应该是同一种,所以如果不判重就会影响最后的答案。我们直接用一个set判重,顺便用到一个巧妙的hash,因为我们一共就只有100000个点,所以我们直接把所有的点都映射到100000+v即可,不会产生冲突。
需要注意的是建完的图是逆拓扑序,按照拓扑序递推DP,所以必须倒着来循环DP。
#include
#include
#include
#include
#include
using namespace std;
const int N = 100007, M = 2000007, INF = 0x3f3f3f3f;
typedef long long ll;
int mod;
int n, m;
int dfn[N], low[N], num;
int ver[M], edge[M], head[N], nex[M], tot;
int h[N];
int Size[N], scc_cnt, scc_id[N];
int stk[N], top, ins[N];
int f[N], g[N];//顶点个数和方案数
void add(int h[],int x,int y){
ver[tot] = y;
nex[tot] = h[x];
h[x] = tot ++ ;
}
void tarjan(int x){
dfn[x] = low[x] = ++ num;
stk[ ++ top] = x;
ins[x] = true;
for(int i = head[x];~i;i = nex[i]){
int y = ver[i];
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x],low[y]);
}
else if(ins[y]){
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]){
int y;
++ scc_cnt;
do{
y = stk[top -- ];
ins[y] = false;
scc_id[y] = scc_cnt;
Size[scc_cnt] ++ ;
}while(x != y);
}
}
int main()
{
memset(head,-1,sizeof head);
memset(h,-1,sizeof h);
scanf("%d%d%d",&n,&m,&mod);
while(m -- ){
int x,y;
scanf("%d%d",&x,&y);
add(head,x,y);
}
for(int i = 1;i <= n;++i)
if(!dfn[i])
tarjan(i);
unordered_set<ll>st;
//(u,v) -> u * 1000000 + v;
//因为一共只有100000个点
for(int i = 1;i <= n;++i){
for(int j = head[i];~j;j = nex[j]){
int k = ver[j];
int a = scc_id[i], b = scc_id[k];
ll hash = a * 100000ll + b;
//建新图并判重
if(a != b && !st.count(hash)){
add(h,a,b);
st.insert(hash);
}
}
}
for(int i = scc_cnt;i >= 1;-- i){//建完的图是逆拓扑序,按照拓扑序递推,所以必须倒着来
if(!f[i]){
f[i] = Size[i];
g[i] = 1;
}
for(int j = h[i];~j;j = nex[j]){
int k = ver[j];
if(f[k] < f[i] + Size[k]){
f[k] = f[i] + Size[k];
g[k] = g[i];
}
else if(f[k] == f[i] + Size[k]){
g[k] = (g[k] + g[i]) % mod;
}
}
}
ll maxx = 0,sum = 0;
for(int i = 1;i <= n;++i){
if(f[i] > maxx){
maxx = f[i];
sum = g[i];
}
else if(f[i] == maxx){
sum = (sum + g[i] ) % mod;
}
}
printf("%lld\n%lld\n",maxx, sum);
return 0;
}
本题与之前的差分约束的那一道糖果题一摸一样,甚至连数据都是一样的。数据实际上比较大,我们可以用差分约束,并将spfa的队列换成栈即可避免超时。
#include
#include
#include
#include
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, size[N];
int dist[N];
void add(int h[], int a, int b, int c)
{
e[idx] = b, w[idx] = c, 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;
size[scc_cnt] ++ ;
} while (y != u);
}
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
memset(hs, -1, sizeof hs);
for (int i = 1; i <= n; i ++ ) add(h, 0, i, 1);
while (m -- )
{
int t, a, b;
scanf("%d%d%d", &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];
if (a == b)
{
if (w[j] > 0)
{
success = false;
break;
}
}
else add(hs, a, b, w[j]);
}
if (!success) break;
}
if (!success) puts("-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;
for (int i = 1; i <= scc_cnt; i ++ ) res += (LL)dist[i] * size[i];
printf("%lld\n", res);
}
return 0;
}