「解题报告」[JSOI2019] 精准预测 (2-SAT)
「解题报告」[JSOI2019] 精准预测 (2-SAT)
题面
题意
有 \(T\) 个时刻, \(n\) 个居民, \(m\) 条预言 \((ty,t,x,y)\), 含义为 :
- \(ty==0\), 在 \(t\) 时刻, 若 \(x\) 是死亡状态, 则在 \(t+1\) 时刻, \(y\) 是死亡状态.
- \(ty==1\), 在 \(t\) 时刻, 若 \(x\) 是生存状态, 则在 \(t\) 时刻, \(y\) 是死亡状态.
每一时刻都可能有居民死去, 有些居民会在 \(0\) 时刻死去. (由你决定)
对每一个居民求出, 有多少个除了他本身的居民有可能和他一起活到 \(T+1\) 时刻.
数据范围
\(T \le 10^6, n\le 5\times10^4,m \le 10^5.\)
思路
\(bool\) 变量 + 限制条件, 看到这两个元素在一起第一反应就应该是 \(2-SAT\).
但毕竟不是板子题, 还是需要一些特殊处理.
让我们分步骤来吧.
建点 & 连边
显然, 我需要把每一个居民拆成代表若干个时刻的点, 如果直接拆成 \(T\) 个点的话显然会炸, 那么对于点 \(u\), 对于每一条预言 \((ty,t,u,v)\), 我们都新建一个时刻为 \(t\) 的点. 全部预言都处理完后, 再对每个点建一个时刻为 \(T+1\) 的点, 表示它最终的生死状态.
然后对于每条预言 \((ty,t,u,v)\), 我们不需要对 \(v\) 建一个时刻为 \(t\) (或 \(t+1\)) 的点, 只需要在 \(v\) 的所有时刻的点中找出第一个时刻 \(\ge t\) (或 \(>t\)) 的点, 并进行连边.
除此之外, 对于每个点 \(u\), 我们还需要在它表示死亡状态的点中, 按照时刻从小到大往后连边; 在它表示死亡状态的点中, 按照时刻从大到小往前连边.
这样, 点数就是 \(2n+2m \le 3 \times 10^5\), 边数为 \(2m+2m \le 4 \times 10^5.\)
统计答案
把图建出来后, 再考虑如何计算答案.
与一般的 \(2-SAT\) 板子不同, 这题不是要求我们判断是否有可行方案或输出一个可行方案, 所以我们就不能直接套 \(Tarjan\).
考虑如何计算答案.
设 \(d'_u,l'_u\) 分别表示点 \(u\) 在时刻 \(T+1\) 时表示死亡状态和生存状态的点.
对于点 \(u\), 设 \(num_u=\sum_{v\in V\ and \ v \not= u} [path(l'_u,d'_v)]\), 其中 \([path(x,y)]=1\) 表示存在一条由 \(x\) 到 \(y\) 的路径. 则 \(ans_u=n-1-num_u.\)
特别地, 对于 \([path(l'_u,d'_u)]=1\) 的点 \(u\), \(ans_u=0\), 并且会使得总体答案 \(-1.\)
假设我们建出来的图是一个 \(DAG\) (有向无环图), 那么我们可以用 \(\rm bitset\) 辅助 \(DP\), 计算出每个点 \(u\) 的 \(num_u\), 总时间复杂度为 \(O\left(\frac{n(2n+2m)}{w}\right) (w=32)\), 约为 \(5\times10^8\).
由于内存限制, \(\rm bitset\) 不能直接开到 \(n\), 所以我们需要分次进行 \(DP\). 虽然看起来 \(\rm bitset\) 的大小和时间复杂度没什么关系, 但为了避免 \(DP\) 初始化,赋值等操作带来的常数, 还是尽量越大越好.上面讨论的是 \(DAG\) 的情况, 而实际上, 我们可以直接证明我们建出来的这张图就是一个 \(DAG\).
证明原图是一个 \(DAG\)
我们考虑把所有点分为 \(d\) (死亡状态), \(l\) (生存状态), 然后分类证明.
为了方便描述, 我们规定
- \(x \rightarrow y\) 表示存在一条从 \(x\) 到 \(y\) 的路径, 且 \(x\) 所代表的时刻小于 \(y\) 所代表的时刻.
- \(y \leftarrow x\) 表示存在一条从 \(x\) 到 \(y\) 的路径, 且 \(x\) 所代表的时刻大于 \(y\) 所代表的时刻.
- \(x \Rightarrow y\) 表示存在一条从 \(x\) 到 \(y\) 的路径, 且 \(x\) 所代表的时刻小于等于 \(y\) 所代表的时刻.
- \(y \Leftarrow x\) 表示存在一条从 \(x\) 到 \(y\) 的路径, 且 \(x\) 所代表的时刻大于等于 \(y\) 所代表的时刻.
按照上文所述的连边方法, 会存在以下这几种边.
- \(d \rightarrow d\)
- \(l \leftarrow l\)
- \(l \Rightarrow d\)
- \(d \Leftarrow l\)
首先考虑 \(d\). 由于不存在从 \(d\) 出发到 \(l\) 的边, 所以若存在一个从 \(d\) 到 \(d\) 的环, 则环上的点一定都是 \(d\). 又因为 \(d\) 只能连向时刻比它大的 \(d\), 所以不可能连回自己. 故不存在一个以 \(d\) 为起点, \(d\) 为终点的环.
再来考虑 \(l\). 同样, 由于不存在从 \(d\) 出发到 \(l\) 的边, 所以若存在一个从 \(l\) 到 \(l\) 的环, 则环上的点一定都是 \(l\), 又因为 \(l\) 只能连向时刻比它小的 \(l\), 所以不可能连回自己. 故不存在一个一 \(l\) 为起点, \(l\) 为终点的环.
故, 原图上不存在环. 即证明了原图是一个 \(DAG\).
那么, 我们就可以按照上述的方法计算出 \(num_u\) 从而计算出 \(ans_u\) 了.
没了
还有一些小细节, 比如为了避免 \([path(l'_u,d'_u)=1\) 的点的影响, 需要做两遍 \(DP\) (本人只想到这种方法), 还有连边时不要把 \(t\) 的关系弄混了.
还有, 这道题需要疯狂卡常, 我这份代码卡到最后也只能随缘过...
代码
#pragma GCC optimize(3) // 手动开了个 O3 优化
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define sz(x) (int)(x).size()
#define mkp make_pair
#define fi first
#define se second
#define lb lower_bound
const int _=5e5+7;
const int X=12500;
bool be;
int n,m,T;
int de[_],tms[_],bel[_],np,num[_],p[_];
int lst[_],nxt[_],to[_],tot;
bool mst[_];
bitset<X+7> f[_];
queue<int> q;
vector<pair<int,int> > ve[_];
struct edge{
int p,t,u,v;
#define p(i) e[i].p
#define t(i) e[i].t
#define u(i) e[i].u
#define v(i) e[i].v
}e[_];
bool en;
bool cmp(edge a,edge b){ return a.t<b.t; }
void add(int x,int y){
nxt[++tot]=lst[y]; to[tot]=x; lst[y]=tot; de[x]++; // 建反向边
}
void _init(){
cin>>T>>n>>m;
for(int i=1;i<=m;i++) cin>>p(i)>>t(i)>>u(i)>>v(i);
sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++){
int u=u(i),t=t(i),sz=sz(ve[u]);
if(!sz||ve[u][sz-1].fi!=t) ve[u].pb(mkp(t,++np)),++np;
}
for(int i=1;i<=n;i++){
ve[i].pb(mkp(T+1,++np)); ++np;
bel[np]=bel[np-1]=i;
p[i]=np;
for(int j=0;j<sz(ve[i])-1;j++){
add(ve[i][j].se,ve[i][j+1].se);
add(ve[i][j+1].se+1,ve[i][j].se+1);
}
}
for(int i=1;i<=m;i++){
int u=u(i),v=v(i),t=t(i);
pair<int,int> tmp=mkp(t,0);
int t1=lb(ve[u].begin(),ve[u].end(),tmp)-ve[u].begin(),t2=lb(ve[v].begin(),ve[v].end(),tmp)-ve[v].begin();
if(!p(i)&&ve[v][t2].fi==t) t2++;
if(p(i)) add(ve[u][t1].se+1,ve[v][t2].se),add(ve[v][t2].se+1,ve[u][t1].se);
else add(ve[u][t1].se,ve[v][t2].se),add(ve[v][t2].se+1,ve[u][t1].se+1);
}
}
void _dp(int L,int R){
for(int i=1;i<=np;i++){
f[i].reset(); tms[i]=0;
if(!de[i]) q.push(i);
}
while(!q.empty()){
int u=q.front(); q.pop();
if(u%2&&!mst[bel[u]]&&bel[u]>=L&&bel[u]<=R) f[u][bel[u]-L]=1;
for(int i=lst[u];i;i=nxt[i]){
int v=to[i];
f[v]|=f[u];
tms[v]++;
if(tms[v]==de[v]){ q.push(v); tms[v]++; }
}
}
}
void _run(){
int ls=0;
for(int i=1;i<=n;i+=X){
_dp(i,i+X-1);
for(int j=i;j<=min(n,i+X-1);j++)
if(f[p[j]][j-i]) mst[j]=1,ls++;
}
for(int i=1;i<=n;i+=X){
_dp(i,i+X-1);
for(int j=1;j<=n;j++) num[j]-=f[p[j]].count();
}
for(int i=1;i<=n;i++)
if(mst[i]) cout<<0<<' ';
else cout<<n-1+num[i]-ls<<' ';
cout<<endl;
}
int main(){
#ifndef ONLINE_JUDGE
freopen("9.in","r",stdin);
freopen("x.out","w",stdout);
#endif
ios::sync_with_stdio(false);
clock_t st=clock();
_init();
st=clock();
_run();
cerr<<(double)(clock()-st)/CLOCKS_PER_SEC<<endl;
cerr<<(double)1.0*(&en-&be)/(1<<20)<<endl;
return 0;
}