[HDU6403]:Card Game(dfs+DP+基环树)
题目传送门
题目描述
她依然在我不知道的地方做我不知道的事。
桌面上摊开着一些卡牌,这是她平时很爱玩的一个游戏。如今卡牌还在,她却不在我身边。不知不觉,我翻开了卡牌,回忆起了当时一起玩卡牌的那段时间。
每张卡牌的正面与反面都各有一个数字,我每次把卡牌按照我想的放到桌子上,而她则是将其中的一些卡牌翻转,最后使得桌面上所有朝上的数字都各不相同。
我望着自己不知不觉翻开的卡牌,突然想起了之前她曾不止一次的让我帮她计算最少达成目标所需要的最少的翻转次数,以及最少翻转达成目标的方案数。
(两种方式被认为是相同的当且仅当两种方式需要翻转的卡牌的集合相同)
如果我把求解过程写成程序发给她,她以后玩这个游戏的时候会不会更顺心一些?
输入格式
每个测试点有多组测试数据。
第一行有一个正整数T表示数据组数。
接下来对于每组数据,第一行有一个正整数n,表示桌子上卡牌的数目。
接下来有n行,每行两个整数x,y, 表示一张两面分别是x,y的卡牌,并且当前朝上的数字为x。
输出格式
对于每组测试数据,输出一行两个整数,分别代表最小的翻转次数、方案数(方案数对998244353取模)。
如果无解则输出“-1 -1”。
样例
样例输入
3
4
1 2
1 3
4 5
4 6
2
1 1
1 1
3
1 2
3 4
5 6
样例输出
2 4
-1 -1
0 1
数据范围与提示
对于20%的数据:
$n \leqslant 20$
$T=1$
对于100%的数据:
$n \leqslant {10}^5$
$T \leqslant 50$
$1 \leqslant x,y \leqslant 2n$
题解
看到这道题,你一定想到了用某种数学方法解决,那么你就凉了。
这么来考虑,将卡片的正面数字向反面数字连边,每次翻转卡牌就相当于是将边反向,而我们的目的就是让图中的每一个联通块内所有点的出度最多为1。
而这时,就分为三种情况:
1.如果图中有一个联通块内的边数大于点数,那么显然不可没能满足,则直接输出“-1 -1”即可。
2.如果图中有一个联通块内的边数等于点数,则这个联通块是一个基环树,那么我们就分别顺时针和逆时针跑这个联通快,看那个方向需要反转变得次数更少,如果两个方向答案相同,则方案数为2,否则为1。
3.如果图中有一个联通块内的边数等于点数-1,则这个联通块是一棵树,那么就考虑使用DP统计答案,但是显然我们需要用这棵树内所有的点为根跑一遍,暴力跑的话时间复杂度显然很高;但是我们发现,对于每一次换根只有它和它的父亲之间的连边会对答案造成影响,而且智慧是上一个答案的+1或是-1,这样我们就可以在时间允许的范围内计算答案了。
代码时刻
#include<bits/stdc++.h>
using namespace std;
struct rec
{
int nxt;
int to;
bool w;//边权为1表示从正面连向反面
}e[200002];
int n;
int head[200001],cnt;
int b,d;
bool vis[200001];
int st,ed,po;
int sta[200001],top;
long long dp[200001],flag[200001];
long long ans,sum,num;
void pre_work()//多测不清空,爆零两行泪TAT……
{
ans=0;
sum=cnt=1;
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
memset(dp,0,sizeof(dp));
memset(flag,0,sizeof(flag));
}
void add(int x,int y,int w)//建边
{
e[++cnt].nxt=head[x];
e[cnt].to=y;
e[cnt].w=w;
head[x]=cnt;
}
void pre_dfs(int x)//跑每一个联通块,计算点数和边数
{
d++;
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt)
{
b++;
if(!vis[e[i].to])
pre_dfs(e[i].to);
}
}
void dfs(int x,int fa)//DP前对每一个联通块进行预处理
{
flag[x]=0;
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt)
{
if(e[i].to==fa)continue;
if(!vis[e[i].to])
{
dfs(e[i].to,x);
flag[x]+=flag[e[i].to]+e[i].w;
}
else
{
st=x;
ed=e[i].to;
po=i;
}
}
}
void pro_dfs(int x,int fa)//DP统计答案
{
sta[++top]=dp[x];
for(int i=head[x];i;i=e[i].nxt)
if(e[i].to!=fa&&i!=po&&i!=(po^1))
{
dp[e[i].to]=dp[x]+(e[i].w?-1:1);//换根之后的+1或者-1
pro_dfs(e[i].to,x);
}
}
int main()
{
int T;
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
pre_work();
for(int i=1;i<=n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y,1);
add(y,x,0);
}
for(int i=1;i<=2*n;i++)
if(!vis[i])
{
b=d=0;
pre_dfs(i);
if(b/2>d)//判断是否有解
{
puts("-1 -1");
goto nxt;//直接跳到结尾,和continue功能类似,但是可以指定跳转位置
}
}
memset(vis,0,sizeof(vis));
for(int i=1;i<=2*n;i++)
{
if(!vis[i])
{
st=ed=po=top=0;
num=1;
dfs(i,0);
dp[i]=flag[i];
pro_dfs(i,0);
if(!st)//如果是一棵树
{
sort(sta+1,sta+top+1);
for(int j=2;j<=top;j++)
{
if(sta[j]!=sta[1])break;
num++;
}
ans+=sta[1];
}
else//是一棵基环树
{
po%=2;
if(dp[st]+po==dp[ed]+(po^1))num=2;
else num=1;
ans+=min(dp[st]+po,dp[ed]+(po^1));
}
sum=sum*num%998244353;
}
}
printf("%lld %lld\n",ans,sum);
nxt:;//上面的"goto nxt;"跳转到的位置
}
return 0;
}
rp++