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\)。这样只有最外层那个套娃会占背包的空间。
称一组套得起来的套娃的浪费空间为这堆结构内部空腔的总体积。显然它等于
其中 \(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)\) 建图 → 前驱是前缀区间 → 区间合并要用线段树」的四级台阶。
建图核心:
- 转化为 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\) 的每条路径对应一个合法子集,路径边权和即浪费空间。
- 排序 + 二分 + 线段树优化:将物品按 \(out_i\) 从小到大排序,能连边的物品是一个连续区间 \([1, R]\),\(R\) 通过二分查找求出。用线段树优化区间连边。线段树父亲向儿子连权值为 \(0\) 的边,叶子节点 \(x\) 向原图节点 \(l\) 连权值为 \(-out[l]\) 的边。
- 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写的题解




本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23068927

浙公网安备 33010602011771号