“蔚来杯“2022牛客暑期多校训练营4 E - Jobs (Hard Version)

Jobs (Hard Version)

昨晚 学弟来问我这道题,就浅浅理A了一下

题意:
给定 n = 1 e 6 n=1e6 n=1e6 个公司, q = 2 e 6 q=2e6 q=2e6个人,每个公司有 m i m_i mi个岗位,且 ∑ i = 1 n m i = 1 e 6 \sum_{i=1}^{n}{m_i}=1e6 i=1nmi=1e6,每个岗位有 A , B , C A,B,C A,B,C 三个属性值,每个人有 a , b , c a,b,c a,b,c 三个属性值。一个人能够胜任这个岗位,当且仅当 A < = a A<=a A<=a && B < = b B<=b B<=b && C < = c C<=c C<=c A , a < = 400 , B , b < = 400 , C , c < = 400 A,a<=400,B,b<=400,C,c<=400 A,a<=400,B,b<=400,C,c<=400)。试求,对于每个人,他能在几个公司任职。(学弟给的题意,如果错了,请当这篇博客不存在

思路:
如果没有公司的限制,只是要求 每个人能够在几个岗位任职,就是简单的 三维偏序问题,直接 C D Q CDQ CDQ 即可,但是有公司的限制,就和种类数类似了,不能考虑 C D Q CDQ CDQ 了。

但是可以借鉴 C D Q CDQ CDQ 的一些思路,比如,我们可以将所有岗位的 A , B , C A,B,C A,B,C 和 人的 a , b , c a,b,c a,b,c 放在一起,按照 a , A a,A a,A 的大小排序,那么 对于每个人,排在他前面的岗位 就是都满足 A A A 的条件的(也即 像 C D Q CDQ CDQ 一样,通过排序降一维)。

这样处理之后,题目就转化成了,序列上 的 pair 对(B,C),对于每个人 j j j,需要求 有多少个公司,满足存在岗位 i, i < j i<j i<j && B i < = B j B_i<=B_j Bi<=Bj && C i < = C j C_i<=C_j Ci<=Cj 。因为求的是种类数,将同种类的岗位放在一起看,可以发现,对于两个不同的岗位 ( B , C ) (B,C) (B,C) ( b , c ) (b,c) (b,c) 如果 B < = b B<=b B<=b C < = c C<=c C<=c ,那么 ( b , c ) (b,c) (b,c) 是被 ( B , C ) (B,C) (B,C) 包含在内的,可以去掉。也即对于同种类的岗位,有效的岗位是一个 B B B升序, C C C降序的一个单调序列。发现了这个性质,那么离做出来也就不远了。

然后,我们看对于每个人 j j j,我们需要求有多少个公司,满足存在岗位 i, i < j i<j i<j && B i < = B j B_i<=B_j Bi<=Bj && C i < = C j C_i<=C_j Ci<=Cj ,把 ( B , C ) (B,C) (B,C) 看成是一个平面,也就是求一个点的左下角有多少种类的点。我们可以维护一个 二维树状数组 t r tr tr t r [ x ] [ y ] tr[x][y] tr[x][y] 代表 ( x , y ) (x,y) (x,y) 的左下角的种类数;那么接下来就是怎么维护种类数了,对于每个公司维护一个单调序列,存 p a i r pair pair 的值,那么每次新进来一个 p a i r pair pair,可能会让对应单调序列 加入一个点 或者 删除一些点,当然不可能 f o r for for 单调序列,可以二分出这个 p a i r pair pair 要插入的位置。然后插入一个点或者删除一个点就在 二维树状数组上进行加减,当然不是在当前点的所有右上角进行加减,而是需要求出当前这个点和 单调序列的 前驱、后继 夹出来的特有矩形,在这上面执行操作。这样就能满足 对于一个特定的公司,对于二维平面上每个点 最多只贡献了 1。
对于一个人只需要在全局的二维树状数组 查询一个给定点的值即可。
(懒得画图了,有心人可以自己结合上文 画图理解一下)

复杂度分析:
M = ∑ i = 1 n m i M=\sum_{i=1}^{n}{m_i} M=i=1nmi
对于每一个岗位都需要在单调序列中二分一下,因为 B < = 400 B<=400 B<=400,所以单调序列中最多只有 400个点,所以这部分的复杂度是 M ⋅ l o g 2 400 M·log_2{400} Mlog2400
对于每个人,都需要在二维树状数组上查询一次值,这部分复杂度是 q ⋅ l o g 2 400 ⋅ l o g 2 400 q·log_2{400}·log_2{400} qlog2400log2400
对于每个岗位在 单调序列上最多只会插入一次、删除一次,所以这部分复杂度是 M ⋅ l o g 2 400 ⋅ l o g 2 400 M·log_2{400}·log_2{400} Mlog2400log2400
可以发现复杂度是 O ( 能过 ) O(能过) O(能过) 的,甚至绰绰有余吧。

代码:
因为笔者退役了,代码需要读者自行完成。
当然上面只是口头理A,还有许多细节需要读者自己思考,比如:
因为等式是 < = <= <= 的,所以排序之后需要 按照 B B B 值相等的分组, 先把所有的岗位都插入了,再去求答案。
(如果学弟 写出来了,再把代码贴着吧)

(op: 学弟终于把题补了)

#include<bits/stdc++.h>
#include<random>
using namespace std;

#define ll long long
//#define int ll

constexpr int P=998244353;
int norm(int x){if(x<0)x+=P;if(x>=P)x-=P;return x;}
template<class T>
T power(T a,ll n){T ans=1;while(n){if(n&1)ans=ans*a;a=a*a;n>>=1;}return ans;}
struct Z
{
	int x;
	Z(int x=0):x(norm(x)){}
	Z(ll x):x(norm(x%P)){}
	int val() const{return x;}
	Z operator-() const{return Z(norm(P-x));}
	Z inv() const{assert(x!=0);return power(*this,P-2);}
	Z&operator*=(const Z&rhs){x=(ll)x*rhs.x%P;return *this;}
	Z&operator+=(const Z&rhs){x=norm(x+rhs.x);return *this;}
	Z&operator-=(const Z&rhs){x=norm(x-rhs.x);return *this;}
	Z&operator/=(const Z&rhs){return *this*=rhs.inv();}
	friend Z operator*(const Z&lhs,const Z&rhs){Z res=lhs;res*=rhs;return res;}
	friend Z operator+(const Z&lhs,const Z&rhs){Z res=lhs;res+=rhs;return res;}
	friend Z operator-(const Z&lhs,const Z&rhs){Z res=lhs;res-=rhs;return res;}
	friend Z operator/(const Z&lhs,const Z&rhs){Z res=lhs;res/=rhs;return res;}
	friend istream&operator>>(istream&is,Z&a){ll v;is>>v;a=Z(v);return is;}
	friend ostream&operator<<(ostream&os,const Z&a){return os<<a.val();}
};

void solve()
{
	int n,q;
	scanf("%d%d",&n,&q);
	vector f(405,vector(405,vector<int>(405)));
	for(int i=1;i<=n;i++)
	{
		 int m;
		 scanf("%d",&m);
		 vector<array<int,3>>v(m);
		 for(int j=0;j<m;j++)
		 {
			 int a,b,c;
			 scanf("%d%d%d",&a,&b,&c);
			 v[j]={a,b,c};
		 }
		 sort(v.begin(),v.end());
		 map<int,int>s;
		 for(auto [a,b,c]:v)
		 {
			 auto it=s.upper_bound(b);
			 if(it!=s.begin()&&prev(it)->second<=c)continue;
			 it=s.lower_bound(b);
			 if(it!=s.begin()&&it!=s.end())
				 f[a][it->first][prev(it)->second]++;
			 while(it!=s.end()&&it->second>=c)
			 {
				 f[a][it->first][it->second]--;
				 if(next(it)!=s.end())
					 f[a][next(it)->first][it->second]++;
				 it=s.erase(it);
			 }
			 auto now=s.emplace(b,c).first;
			 f[a][b][c]++;
			 if(it!=s.end())f[a][it->first][c]--;
			 if(now!=s.begin())f[a][b][prev(now)->second]--;
		 }
	}
	for(int i=2;i<=400;i++)
		for(int j=1;j<=400;j++)
			for(int k=1;k<=400;k++)
				f[i][j][k]+=f[i-1][j][k];
	for(int i=1;i<=400;i++)
		for(int j=2;j<=400;j++)
			for(int k=1;k<=400;k++)
				f[i][j][k]+=f[i][j-1][k];
	for(int i=1;i<=400;i++)
		for(int j=1;j<=400;j++)
			for(int k=2;k<=400;k++)
				f[i][j][k]+=f[i][j][k-1];
	int seed;
	scanf("%d",&seed);
	mt19937 rng(seed);
	uniform_int_distribution<> u(1,400);
	int lastans=0;
	Z ans=0;
	for(int i=1;i<=q;i++)
	{
		int IQ=(u(rng)^lastans)%400+1;  // The IQ of the i-th friend
		int EQ=(u(rng)^lastans)%400+1;  // The EQ of the i-th friend
		int AQ=(u(rng)^lastans)%400+1;  // The AQ of the i-th friend
		lastans=f[IQ][EQ][AQ];  // The answer to the i-th friend
		ans=ans*seed+Z(lastans);
	}
	printf("%d\n",ans);
	
}

signed main()
{
//	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	
	
	
//	int _;scanf("%d",&_);while(_--)
		solve();
	return 0;
}

碎碎:
昨天晚上理A之后给学弟讲完,也算是感概万千,可能如果不退役,自己也能成为一名出色的金牌选手。
这几天,也不知道自己在纠结什么,不知道是后悔还是不甘,也不知道自己喜欢acm是因为 做出题的愉悦,还是因为自己打的还算不错的底子,抑或是这两年在集训队的点滴。

须知少时凌云志 曾许人间第一流

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值