图论好题。采用正难则反的思想,先将所有边连接起来。如果连接后存在某些点的度数不足 $k$,则不可能对答案有贡献,删除。删除后剩下的点就是最后一晚的人数。接下来从第 $n-1$ 晚开始往前遍历,每次删除一条边,即可得到每晚的人数,最后倒序输出。
如果像上面那样操作,删除指定点或边后,如何保证剩下的点度数一定不小于 $k$?
很简单。关于删除点,可以将点放入队列中,最后遍历队列,将与遍历到的点直接相连的点度数 $-1$,如果减了之后不足 $k$,则该点需要删除,放入队列中。
关于删除边,将边的两端点度数分别 $-1$,如果度数 $<k$,将点入队,如删除点操作即可。
有一个需要注意的点是可能存在因遍历队列而导致的一条边被减多次,这里的解决方式是使用 $\text{map}$ 来标记每条边是否已经拆散。
#include<iostream>
#include<cstdio>
#include<unordered_map>
using namespace std;
const int N=2e5+100;
typedef long long ll;
int n,m,c,k,t[N],ans[N],d[N],cnt,l=1,r;
ll u[N],v[N],q[N];
unordered_map<ll,bool> um;
struct node
{
int last,id;
}a[N*2];
void add(int a1,int a2)
{
a[++k].id=a2;
a[k].last=t[a1];
t[a1]=k;
}
void del()//删除队列中的点
{
while(l<=r)
{
d[q[l]]=0;
for(int i=t[q[l]];i;i=a[i].last)//遍历与当前点直接相连的点
{
if(um[q[l]*100000+a[i].id]) continue;
d[a[i].id]--;
if(d[a[i].id]==c-1) q[++r]=a[i].id,cnt--;//度数<k,需要删除,入队
}
l++;
}
}
int main()
{
scanf("%d%d%d",&n,&m,&c);
cnt=n;
for(int i=1;i<=m;i++)
{
scanf("%lld%lld",&v[i],&u[i]);
add(u[i],v[i]);
add(v[i],u[i]);
d[v[i]]++,d[u[i]]++;
}
for(int i=1;i<=n;i++)
{
if(d[i]<c)
{
cnt--;
q[++r]=i;
}
}
del();//删除度数不足k的点
ans[m]=cnt;
for(int i=m;i>=2;i--)
{
um[u[i]*100000+v[i]]=true;
um[v[i]*100000+u[i]]=true;
if(d[u[i]]<=0||d[v[i]]<=0)
{
ans[i-1]=cnt;
continue;
}
//删边
d[v[i]]--,d[u[i]]--;
if(d[u[i]]==c-1) q[++r]=u[i],cnt--;
if(d[v[i]]==c-1) q[++r]=v[i],cnt--;
del();
ans[i-1]=cnt;
}
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}