div.3后三题题解

F. Programming Competition

一道非常有意思的题目,要解决这个问题首先我们需要能够发现以下几个特性

1,每个人都一定可以和不同分支的人组队,注意!是可以,而不是只可以!他们也能和同样分支的人组队,只要不和上司组就行,为了描述的更加清楚,放一张图

BOSS下面可能有很多分支,每个不同分支的人都可以组队,但是同样分支的人在满足某种情况下也是可以组队的,这里的BOSS并不单只1号节点,它可以是任何父节点(这里每个圆圈代表一个分支)

2,我们需要求出其中最大的分支的数量(我们可以先把每个节点的子节点加上他自己有多少个预处理出来)

第一种情况:假如最大分支的数量大于其他分支的总和,那就意味着其他的所有分支都必定可以找到人组队,因为在不济也可以在那个最大的分支里面挑

第二种情况:假如最大分支的数量小于其他分支的总和,这就意味这他们全部的人都可以通过一系列骚操作找到人组队!(当然提前是偶数,是奇数的话可能要有一个孤儿了,不过也不用特殊处理,因为计算机会自动帮我们下取整)

证明如下:当前最大的分支总是找到第二大的那个分支把第二大的给组完

放个图举个例子

10把8组完,6把3组完,3把2组完,余一个孤儿(因为是奇数)

我们可以发现上面两种情况要是第二种情况就方便了,直接就all in了,但是第一种还存在分支内部组队的情况,所以我们着重对第一种情况处理,我们可以把已经确定可以组队的人数用cnt存起来,然后把那个最大的分支的第一个节点当成BOSS处理,要是还是第一种情况就把人数再次加到cnt里面,直到找到第一种情况我们之间all in返回,所以这里就是很明显递归dfs了;为了描述方便,我们称第一种情况的最大分支为超大分支

上代码

#include<iostream>
#include<vector>
using namespace std;
const int N=200010;
vector<int>a[N];
int ans;
int son[N];
void dfs1(int u)//把每个节点他的下属加上他自己的数量预处理出来
{
    son[u]=1;
    for(int v:a[u])
    {
        dfs1(v);
    son[u]+=son[v];
    }
    return;
}
void dfs2(int u,int cnt)//cnt用来存目前攒了多少可以组队的人数
{
    if(cnt)//这里代表那个超大分支的Boss需要从cnt里面拿出一个人来和他组队,因为他不会参与到后面的计数中
    {
        cnt--;
        ans++;
    }
    int s=son[u]-1;用来存u节点的所有下属数量
    int idx=-1,ma=0;//idx存最大分支的编号
    for(int v:a[u])//遍历u节点的所有分支
    {
        if(son[v]>ma)
        {
            ma=son[v];
            idx=v;
        }
    }
    if(ma*2<=s)判断是不是超大分支,如果不是直接all in return了
    {
        ans+=(cnt+s)/2;
        return;
    }
    dfs2(idx,cnt+s-ma);如果是继续找该超大分支的超大分支,直到找不到为止,并且把除了超大分支其他分支的数量都加到cnt里
}
int main()
{
    int t;cin>>t;
    while(t--)
    {
        int n;cin>>n;
        for(int i=1;i<=n;i++)a[i].clear();
        for(int i=2;i<=n;i++)
        {
            int x;cin>>x;
            a[x].push_back(i);//建图,当然这里用邻接表也可以
        }
        ans=0;
        dfs1(1);//预处理
        dfs2(1,0);//从1号点开始搜,一个人也没攒
        cout<<ans<<endl;
    }
    return 0;
}

G1&G2. Light Bulbs (Hard Version)

这题第一问倒是煎蛋,第二问就不会了。看的官方题解好像要用XOR哈希,临时去学了下hhhh。首先引入一个概念方便后续描述,就是对于一个连续的并且其中的某种颜色的灯只包含2个的区间称为封闭段,比如[1,1,2,2,3,3],为封闭段,而[1,2,3,4]不是,因为不是每种颜色都有两个,并且将不可被分成多个封闭段封闭段的称为最小封闭段,比如[1,1,2,2,3,3]就不是因为它可以分成[1,1],[2,2],[3,3]多个封闭段,但是[3,2,1,2,1,3]是最小封闭段

由题目所给性质易证,每个封闭段内的变化都无法影响到封闭段之外的区间,而最小封闭段,必然存在只开启某个灯就可以让该封闭段所有的灯亮起来,这种灯记为最优灯,那么第一问就是求给出的区间一共有多少个最小封闭段就行了,这就是第一问的答案,然后第二问就是求每个封闭段中有多少个最优灯,然后把它们相乘就是方案数了,于是就有两个难点,第一个是如何求有多少个最小封闭段,第二个就是怎么计算最优灯,其实第二个问题和第一个是类似的,拿[3,2,1,2,1,3]的灯举例,我们可以发现只有3是最优灯,而[2,1,2,1]都不行,因为[2,1,2,1]本身就是一个最小封闭段,它是不会影响它之外的灯的,所以问题就变成了,最最小封闭段内除去边界再求一遍最小封闭段,然后把它们打上记号,剩下的就都可以是最优灯了,在这里分享一个不用哈希时间复杂度也是O(n)的算法,第一问可以顺利求出来,第二问可能也行,但是边界问题太多了,调了半天还一直被卡,太麻烦然后我就放弃了,看答案还是哈希香,就是用map记录每个每个节点的第二个出现的位置,然后从第头遍历,每遍历一个点如果map[a[i]]大于end,更新一下end的值,(end表示的是当前封闭段最后位置)直到走到end为止,那么这就是一个最小封闭区间,

简单说一下XOR哈希的原理,就是对每个颜色的灯生成一个随机数,然后从头遍历,然后用一个数x来表示当前状态,x异上当前颜色的随机数,如果当x为0时就说明前面是一个最小封闭区间,因为,上两个相同的数抵消等于0

来看代码

#include<bits/stdc++.h>
using namespace std;
const int mod=998244353;
mt19937_64 rnd(98275314);
vector<int>c;//存数据
vector<int>g;//这个数组非常关键,用来....算了讲不清楚,往后面看
long long gen()//这个和第三行生成随机数据
{
	long long x=0;
	while(x==0)
		x=rnd();
	return x;
}
int f(int l,int r)//最关键的函数用来找最优灯的个数
{
	int res=0;//记最优灯的数量
	while(l<r)
	{
		if(g[l]!=-1&&g[l]<r)
			l=g[l];//算最优灯的时候因为l与g[l]的状态一样说明然后他们自己就是一个最小封闭段,直接跳过即可,提前是g[l]!=-1,这样才可以跳;
		else
		{
			res++;//其他所有的都是最优灯
			l++;
		}
	}
	return res;
}
void solve()
{
	int n;cin>>n;
	long long cnt=0,ans=1;//cnt存最小封闭段个数,ans存方案
	c.resize(n*2);
	g.resize(n*2,-1);//将g数组初始为-1;
	for(int i=0;i<2*n;i++)scanf("%d",&c[i]);
	vector<long long>a(n+1);
	for(int i=1;i<=n;i++)a[i]=gen();//a数组用来存储颜色为i的随机数据
	map<long long,int>m;
	long long x=0;
	m[0]=0;//第一个最小封闭段起始为0
	for(int i=0;i<n*2;i++)
	{
		x^=a[c[i]];//把当前的状态用x表示
		if(x==0)一旦x=0,说明当前区间所有的灯都是两两成对,找到了最小封闭段
		{
			cnt++;
			ans=(ans*f(m[0],i+1)%mod+mod)%mod;
			m.clear();算完一个最小封闭段记得清空map
		}
		else if(m.count(x))//如果状态x已经被map记录过了,而此时却又出现,说明这个状态和之前map[x]之间也是一个最小封闭段,然后用g来存当前下标
		{
			g[m[x]]=i+1;//这里都要记下标加1,我就在这个地方卡了半个小时,
//一开始我也不理解为什么加1,但是后面想通了,举个例子[3,2,1,2,1,3]4,我们算了前一个最小封闭段
//准备算下一个的时候,此时x在灯3的状态为0,但是我们要从灯4开始,所以得加上1
		}
		m[x]=i+1;
	}
	cout<<cnt<<' '<<ans<<endl;
	c.clear();
	g.clear();
}
int main()
{
	int t;cin>>t;
	while(t--)solve();
	return 0;
}

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值