P1197E. 套娃密码题解

P1197E. 套娃密码 (Culture Code)

题目描述

附近的纪念品商店在卖著名的俄罗斯套娃(matryoshka)。店里有 \(n\) 个不同的套娃,第 \(i\)个套娃的外体积为 \(out_i\),内部空腔体积为 \(in_i\)(保证\(in_i<out_i\))。

套娃可以一个套一个。形式化地说,称一组套娃是套得起来的,如果能把它们重排成 \(i_1,i_2,…,i_k\) 使得第一个能放进第二个、第二个能放进第三个……以此类推。套娃 \(i\) 能放进套娃 \(j\) 当且仅当 \(out_i≤in_j\)。这样只有最外层那个套娃会占背包的空间。

称一组套得起来的套娃的浪费空间为这堆结构内部空腔的总体积。显然它等于

\[in_{i_1}+(in_{i_2}-out_{i_1})+(in_{i_3}-out_{i_2})+…+(in_{i_k}-out_{i_{k-1}}) \]

其中 \(i_1,i_2,…,i_k\) 是被选出的套娃按「从里到外」排列的下标。

最后,称一个套得起来的子集是极大的,如果给定集合中不存在任何一个套娃能够在不破坏「套得起来」的前提下加进这个子集。

你想买的套娃越多越好,所以你会挑一个极大的套得起来的子集。但你也不想浪费太多背包空间,所以你要在所有极大的子集里选浪费空间最小的那些。现在你想知道:满足「极大」且「浪费空间取到全局最小值」的子集有多少个?

两个子集被认为不同,当且仅当存在至少一个下标 \(i\),使得一个子集包含第 \(i\)个套娃而另一个不包含。答案可能很大,请对 \(10^9+7\) 取模。

输入格式

第一行一个整数 \(n\)(\(1≤n≤2⋅10^5\))——套娃个数。

接下来 \(n\) 行,每行两个整数 \(out_i\)和 \(in_i\)(\(1≤in_i<out_i≤10^9\))——第 \(i\)个套娃的外体积和内部空腔体积。

输出格式

输出一行一个整数——满足条件的子集个数对 \(10^9+7\)取模的结果。

样例

输入

7
4 1
4 2
4 2
2 1
5 4
6 4
3 2

输出

6

说明

样例中有 66 个浪费空间最小的极大套娃子集:

  • {1,5}:再加任何套娃都不行,浪费空间为 1;
  • {1,6};
  • {2,4,5};
  • {2,4,6};
  • {3,4,5};
  • {3,4,6}。

还有别的「极大」子集,比如 {4,6,7},但它的浪费空间是 2,不是最小值;而 {6,7} 不是极大的(还能把第 4 个套娃塞进去)。

数据范围

本题共 25 个测试点,分 4 个子任务,合计 100 分。每个测试点只有一组数据。

子任务 分值 约束条件
子任务 1 \(20\) \(n≤18\)
子任务 2 \(25\) \(n≤3000\)
子任务 3 \(25\) \(n≤2⋅10^5\),且每个套娃最多只能装下 50 个不同的套娃(即对任意 \(j\),满足 \(out_i≤in_j\) 的 \(i\)不超过 50 个)
子任务 4 \(30\) \(n≤2⋅10^5\),无额外限制

把「套娃」抽象成点、「\(i\)能放进 \(j\)」抽象成有向边 \(i→j\),整张图是一个 DAG。再补上两个虚拟点: 一个源点连向所有「装不进任何别的套娃」的点,所有「装不下任何东西」的点连向一个汇点。 于是每条 \(s→t\) 的路径恰好对应一个极大子集,路径长度就是浪费空间——问题变成 DAG 最短路计数。 但这样直接建图有 \(O(n^2)\) 条边,这正是本题要解决的核心矛盾:

子任务 1:\(n≤18\),\(2^n\)枚举所有子集、逐个检查「套得起来 + 极大」即可,不需要建图。(20分)

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=998244353;
const int inf=1e18;
bool qqq(pair<int,int> a,pair<int,int> b){return a.fi>b.fi;}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin>>n;
    if(n<=18)
    {
    	vector<pair<int,int>> q(n+1);
    	map<int,int> w;
		map<pair<int,int>,int> r;
    	for(int i=1;i<=n;i++)
    	{
    		cin>>q[i].fi>>q[i].se;
    		if(w.find(q[i].fi)!=w.end()) w[q[i].fi]++;
    		else w[q[i].fi]=1;
    		r[{q[i].fi,q[i].se}]=1;
		}
		for(int i=1;i<=n;i++)
		{
			for(int l=1;l<=n;l++)
			{
				if(l==i) continue;
				if(q[l].se>=q[i].fi)
				{
					r[{q[i].fi,q[i].se}]=0;
					break;
				}
			}
		}
		int num=inf,ans=0;
		for(int i=1;i<(1<<n);i++)
		{
			vector<pair<int,int>> e;
			for(int l=1;(1<<l-1)<=i;l++) if((1<<l-1)&i) e.push_back({q[l].fi,q[l].se});
			e.push_back({0,0});
			sort(e.be,e.en,qqq);
			bool p=0;
			for(int l=0;l<e.size()-1;l++)
			{
				if(e[l].fi==e[l+1].fi||e[l].se<e[l+1].fi) 
				{
					p=1;
					break;
				}
			}
			if(r[{e[0].fi,e[0].se}]==0) continue;
			if(p==1) continue;
			for(int l=0;l<e.size()-1;l++)
			{
				if(e[l].se!=e[l+1].fi)
				{
					for(int j=e[l].se-1;j>e[l+1].fi;j--) if(w.find(j)!=w.end()) if(w[j]>0)
					{
						p=1;
						break;
					}
				}
				if(p==1) break;
			}
			if(p==1) continue;
			int cnt=0;
			for(int l=0;l<e.size()-1;l++) cnt+=e[l].se-e[l+1].fi;
			if(cnt<num) 
			{
				num=cnt;
				ans=0;
			}
			if(num==cnt) ans++;
		}
		cout<<ans;
	}
	else cout<<"0";
    return 0;
}

子任务 2:\(n≤3000\),\(O(n^2)\)条边还能承受,写出「枚举所有可嵌套对建图 + 按拓扑序做最短路计数」就能过。(45分)

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=1e9+7;
const int inf=1e18;
struct node
{
	int out,in;
	bool operator<(const node& o)const{return out<o.out;}
};
bool qqq(pair<int,int> a,pair<int,int> b){return a.fi>b.fi;}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n;
    cin>>n;
    if(n<=18)
    {
    	vector<pair<int,int>> q(n+1);
    	map<int,int> w;
		map<pair<int,int>,int> r;
    	for(int i=1;i<=n;i++)
    	{
    		cin>>q[i].fi>>q[i].se;
    		if(w.find(q[i].fi)!=w.end()) w[q[i].fi]++;
    		else w[q[i].fi]=1;
    		r[{q[i].fi,q[i].se}]=1;
		}
		for(int i=1;i<=n;i++)
		{
			for(int l=1;l<=n;l++)
			{
				if(l==i) continue;
				if(q[l].se>=q[i].fi)
				{
					r[{q[i].fi,q[i].se}]=0;
					break;
				}
			}
		}
		int num=inf,ans=0;
		for(int i=1;i<(1<<n);i++)
		{
			vector<pair<int,int>> e;
			for(int l=1;(1<<l-1)<=i;l++) if((1<<l-1)&i) e.push_back({q[l].fi,q[l].se});
			e.push_back({0,0});
			sort(e.be,e.en,qqq);
			bool p=0;
			for(int l=0;l<e.size()-1;l++)
			{
				if(e[l].fi==e[l+1].fi||e[l].se<e[l+1].fi) 
				{
					p=1;
					break;
				}
			}
			if(r[{e[0].fi,e[0].se}]==0) continue;
			if(p==1) continue;
			for(int l=0;l<e.size()-1;l++)
			{
				if(e[l].se!=e[l+1].fi)
				{
					for(int j=e[l].se-1;j>e[l+1].fi;j--) if(w.find(j)!=w.end()) if(w[j]>0)
					{
						p=1;
						break;
					}
				}
				if(p==1) break;
			}
			if(p==1) continue;
			int cnt=0;
			for(int l=0;l<e.size()-1;l++) cnt+=e[l].se-e[l+1].fi;
			if(cnt<num) 
			{
				num=cnt;
				ans=0;
			}
			if(num==cnt) ans++;
		}
		cout<<ans;
		return 0;
	}
    vector<node> q(n+1);
    for(int i=1;i<=n;i++) cin>>q[i].out>>q[i].in;
    sort(q.be,q.en);
    vector<vector<int>> a(n+2);
    vector<int> de(n+2,0);
    // 0是源点,n+1是汇点
    for(int i=1;i<=n;i++)
    {
    	for(int j=1;j<=n;j++)
    	{
    		if(j==i) continue;
    		if(q[j].out<=q[i].in)
    		{
    			a[j].push_back(i);
    			de[i]++;
			}
		}
		if(de[i]==0)
		{
			a[0].push_back(i);
			de[i]++;
		}
		bool p=0;
		for(int j=1;j<=n;j++)
		{
			if(j==i) continue;
			if(q[i].out<=q[j].in)
			{
				p=1;
				break;
			}
		}
		if(!p)
		{
			a[i].push_back(n+1);
			de[n+1]++;
		}
	}
	de[0]=0;
	vector<int> di(n+2,inf),cnt(n+2,0);
	di[0]=0;
	cnt[0]=1;
	queue<int> w;
	w.push(0);
	while(!w.empty())
	{
		int u=w.front();
		w.pop();
		for(int v:a[u])
		{
			if(di[u]+q[u].in-q[v].out<di[v])
			{
				di[v]=di[u]+q[u].in-q[v].out;
				cnt[v]=cnt[u];
			}
			else if(di[u]+q[u].in-q[v].out==di[v]) cnt[v]=(cnt[v]+cnt[u])%mod;
			if(--de[v]==0) w.push(v);
		}
	}
	cout<<cnt[n+1]%mod<<"\n";
    return 0;
}

子任务 3:\(n\) 涨到 \(2⋅10^5\),但数据保证总边数 \(≤50n\)。此时把套娃按 \(out\)排序后,每个点的前驱恰好是一段前缀区间, 用二分定位这段区间、再枚举区间里的点即可(因为区间长度 \(≤50\))。这一步会让人意识到:转移其实就是「对一段前缀做合并」。

因为子任务 3与子任务 4的范围一样,就不额外写了,(其实是懒得写了),代码先欠着

子任务 4:边数放开到 \(O(n^2)\),前缀区间不能枚举了。于是必须把这个「区间合并」做到 \(O(\log⁡ n)\)—— 也就是本场最核心的技巧:线段树优化建图(在 DP 上等价于「线段树维护 (最小值, 方案数) 的区间合并」)。

点击查看代码
#include <bits/stdc++.h>
#define int long long
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
using namespace std;
const int N=2e5+5;
const int mod=1e9+7;
const int inf=1e18;
struct node
{
	int out,in;
	bool operator<(const node& o)const{return out<o.out;}
};
vector<pair<int,int>> e[N<<2];
vector<node> q;
int tot,rt1,rt2,lc[N<<2],rc[N<<2],d[N<<2],n;
inline int ms(int l,int r){return l+(r-l>>1);}
inline void add(int x,int y,int v)
{
	d[y]++;
	e[x].push_back({y,v});
}
inline void build(int &x,int l,int r)
{
	x=++tot;
	if(l==r)
	{
		add(x,l,-q[l].out);
		return ;
	}
	int mid=ms(l,r);
	build(lc[x],l,mid);
	build(rc[x],mid+1,r);
	add(x,lc[x],0);
	add(x,rc[x],0);
}
inline void update(int x,int l,int r,int nl,int nr,int i)
{
	if(nl<=l&&r<=nr)
	{
		add(i,x,q[i].in);
		return ;
	}
	int mid=ms(l,r);
	if(nl<=mid) update(lc[x],l,mid,nl,nr,i);
	if(nr>mid) update(rc[x],mid+1,r,nl,nr,i);
}
signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin>>n;
    tot=n;
    q.resize(n+1);
    int maxx=0;
	for(int i=1;i<=n;i++) 
	{
		cin>>q[i].out>>q[i].in;
		maxx=max(maxx,q[i].in);
	}
	sort(q.be+1,q.be+1+n);
	build(rt1,1,n);
	vector<int> a(n+1);
	for(int i=1;i<=n;i++) a[i]=q[i].out;
	++tot;
	for(int i=1;i<=n;i++)
	{
		if(q[i].out>maxx) add(0,i,0);
		if(q[i].in<a[1]) add(i,tot,q[i].in);
		int pos=upper_bound(a.be+1,a.be+n+1,q[i].in)-a.be-1;
		if(pos==0) continue;
		update(rt1,1,n,1,pos,i);
	}
	vector<int> dis(tot+1,inf);
	queue<int> w;
	for(int i=0;i<=tot;i++) if(!d[i]) 
	{
		w.push(i);
		//dis[i]=0;
	}
	vector<int> cnt(tot+1,0);
	cnt[0]=1;
	dis[0]=0;
	while(!w.empty())
	{
		int x=w.front();
//		cout<<"ASDasd  "<<x<<"\n";
		w.pop();
		for(auto o:e[x])
		{
			int y=o.fi,v=o.se;
			if(dis[x]!=inf) 
			{
				if(dis[x]+v<dis[y])
				{
					dis[y]=dis[x]+v;
					cnt[y]=cnt[x];
				}
				else if(dis[x]+v==dis[y]) cnt[y]=(cnt[y]+cnt[x])%mod;
			}
			
			if(--d[y]==0) w.push(y);
		}
	}
	cout<<cnt[tot]%mod<<"\n";
//	for(int i=0;i<=tot;i++) cout<<cnt[i]<<" ";
    return 0;
}

官方代码

点击查看代码
#include<bits/stdc++.h>
#define x first
#define y second
using namespace std;
const int inf=1e9+555;
const int mod=1e9+7;
int norm(int a) 
{
	if(a>=mod) a-=mod;
	if(a<0) a+=mod;
	return a;
}
pair<int,int> combine(const pair<int,int> &a,const pair<int,int> &b) 
{
	if(a.x<b.x) return a;
	if(a.x>b.x) return b;
	return {a.x,norm(a.y+b.y)};
}
int n;
vector<pair<int,int>> p,T;
void setVal(int pos,const pair<int,int> &val) 
{
	T[pos+=n]=val;
	for(pos>>=1;pos>0;pos>>=1) T[pos]=combine(T[2*pos],T[2*pos+1]);
}
pair<int,int> getMin(int l,int r) 
{
	pair<int,int> ans={inf,0};
	for(l+=n,r+=n;l<r;l>>=1,r>>=1) 
	{
		if(l&1) ans=combine(T[l++],ans);
		if(r&1) ans=combine(T[--r],ans);
	}
	return ans;
}
int main() 
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	cin>>n;
	p.resize(n);
	for(int i=0;i<n;i++) cin>>p[i].x>>p[i].y;
	auto comp=[](const pair<int,int> &a,const pair<int,int> &b) 
	{
		if(a.y!=b.y) return a.y<b.y;
		return a.x<b.x;
	};
	sort(p.begin(),p.end(),comp);
	T.assign(2*n,{inf,0});
	for(int i=n-1;i>=0;i--) 
	{
		int pos=lower_bound(p.begin(), p.end(), pair<int,int>(0, p[i].x),comp)-p.begin();
		if(pos>=n) 
		{
			setVal(i,{p[i].y,1});
			continue;
		}
		pair<int,int> bst=getMin(pos, n);
		setVal(i,{bst.x-(p[i].x-p[i].y),bst.y});
	}
	cout<<getMin(0,n).y<<endl;
	return 0;
}

四档正好是「暴力 → \(O(n^2)\) 建图 → 前驱是前缀区间 → 区间合并要用线段树」的四级台阶。

建图核心:

  1. 转化为 DAG 上最短路与路径计数:对于每个物品 \(i\),找到能套入 \(i\) 中的物品 \(j\)(即 \(out_j < in_i\)),从 \(i\) 向 \(j\) 连一条边,边权为 \(in_i - out_j\)。建虚拟源点 \(s\) 和汇点 \(t\),\(s\) 向入度为 \(0\) 的点连边权 \(0\) 的边,出度为 \(0\) 的点向 \(t\) 连边权 \(in_i - out_j\) 的边。DAG 上 \(s\) 到 \(t\) 的每条路径对应一个合法子集,路径边权和即浪费空间。
  2. 排序 + 二分 + 线段树优化:将物品按 \(out_i\) 从小到大排序,能连边的物品是一个连续区间 \([1, R]\),\(R\) 通过二分查找求出。用线段树优化区间连边。线段树父亲向儿子连权值为 \(0\) 的边,叶子节点 \(x\) 向原图节点 \(l\) 连权值为 \(-out[l]\) 的边。
  3. DAG 上最短路 + 路径计数:拓扑排序后按拓扑序做最短路和路径计数 DP。记 \(dist[x]\) 为 \(s\) 到 \(x\) 的最短路,\(cnt[x]\) 为最短路条数,若 \(dist[y] > dist[x] + w\)(x,y),则更新 \(dist[y]\) 并重置 \(cnt[y]\);若 \(dist[y] = dist[x] + w(x,y)\),则 \(cnt[y] += cnt[x]\)。

以下是我老师用AI写的题解

posted @ 2026-09-21 21:05  Aries2011  阅读(19)  评论(0)    收藏  举报