A - 区间选点 II
题意
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
思路
本题使用差分约束系统求解。
构造不等式组来建立差分约束。记sum[i]为数轴上[0,i]之间选点的个数,则对于第i个区间[ai,bi],可以建立起不等式sum[bi]-sum[(ai)-1]≥ci;除此之外,为保证sum有意义,sum数组要满足0≤sum[i]-sum[i-1]≤1。如此,只要将该差分系统转化为求图的最长路即可解决。
利用链式前项星建立起有向图,因为图中有负边,利用SFPA求单源最长路。最终sum[max[bi]]即最小选点数。
实际编程时遇到问题:当ai=0甚至bi=0时,原公式无法处理。解决方法是建立一个“超级源点”super,当ai=0时,该区间表示的边从super连接到bi;特别的,当一组输入没有涉及到super时,插入边(0,0)。如此就实现了输入的统一表示。
Debug
首次提交TLE,意识到可能是长期以来没用前向星而是一直用链表表示图,今天终于被卡了。改用前向星实现代码。
再次提交RE,反复检查找不到越界。尝试性地扩大了所有数组的数量级后意外通过了,仔细检查之后发现问题出在前向星的数组大小:只注意到输入边数为5000,没考虑到为了保证sum有意义还需的2n条边,开的edges[5005]导致了越界。
代码
#include<iostream>
#include<queue>
#define INF -1000000
using namespace std;
int n,cotE;
int a[500005],b[500005],c[500005];
int dis[500005],inq[500005],reach[500005];
int maxB;
queue<int> Q;
struct edge{
int to,next,weight;
}edges[500005];
int head[500005];
int super;
void addEdge(int u,int v,int w){
++cotE;
edges[cotE].to=v;
edges[cotE].weight=w;
edges[cotE].next=head[u];
head[u]=cotE;
}
void addSuper(int v,int w){
++cotE;
edges[cotE].to=v;
edges[cotE].weight=w;
edges[cotE].next=super;
super=cotE;
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=0;i<=50000;i++){
dis[i]=INF;
}
int jdg=0;
maxB=0;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i]>>c[i];
if(b[i]==0)jdg=1;
if(b[i]>b[maxB])maxB=i;
if(a[i]==0){
addSuper(b[i],c[i]);
}else{
addEdge(a[i]-1,b[i],c[i]);
}
}
for(int i=1;i<=b[maxB];i++){//不止50005!
addEdge(i-1,i,0);
addEdge(i,i-1,-1);
}
if(!jdg)addSuper(0,0);
for(int i=super;i>0;i=edges[i].next){//初始入队
dis[edges[i].to]=edges[i].weight;
Q.push(edges[i].to);
inq[edges[i].to]=1;
}
while(!Q.empty()){
int u=Q.front();
Q.pop();
inq[u]=0;
for(int i=head[u];i>0;i=edges[i].next){
if(dis[edges[i].to]<dis[u]+edges[i].weight){
dis[edges[i].to]=dis[u]+edges[i].weight;
if(!inq[edges[i].to]){
Q.push(edges[i].to);
inq[edges[i].to]=1;
}
}
}
}
cout<<dis[b[maxB]]<<endl;
}
B - 猫猫向前冲
题意
众所周知, TT 是一位重度爱猫人士,他有一只神奇的魔法猫。
有一天,TT 在 B 站上观看猫猫的比赛。一共有 N 只猫猫,编号依次为1,2,3,…,N进行比赛。比赛结束后,Up 主会为所有的猫猫从前到后依次排名并发放爱吃的小鱼干。不幸的是,此时 TT 的电子设备遭到了宇宙射线的降智打击,一下子都连不上网了,自然也看不到最后的颁奖典礼。
不幸中的万幸,TT 的魔法猫将每场比赛的结果都记录了下来,现在他想编程序确定字典序最小的名次序列,请你帮帮他。
思路
本题使用拓扑序列。
为了保证字典序最小输出,使用了最小堆。在初始化完胜负关系后,将所有入度为0的猫猫添入堆。每次出堆,访问该猫表示的节点可以到达的所有节点,检查它们能否入堆。
为了快速确定入度为0的猫猫,开辟了辅助数组in[],每添加一对胜负关系,给入点的in++,这样,在每次取猫的时候只要入度自减到了0,就可以入堆。
准备一个名次数组,每次出堆时,利用计数将该猫标记为下一位胜者,直到将所有的猫全部在名次数组中归位。
代码
#include<iostream>
#include<queue>
using namespace std;
priority_queue<int,vector<int>,greater<int> > heap;
int lose[505];//输的次数(入度)
int win[505];//胜者序列
int N,M,cot;
struct Node{
int v;
Node* next;
};
Node* cat[505];
void addEdge(int u,int v){
Node* theNode=new Node;
theNode->v=v;
Node* p=cat[u];
Node* q;
bool jdg=0;
while(p!=NULL){
if(p->v>v)break;
jdg=1;
q=p;
p=p->next;
}
theNode->next=p;
if(jdg)q->next=theNode;
else cat[u]=theNode;
}
int main(){
while(cin>>N){
cin>>M;
cot=0;
for(int i=1;i<=N;i++){
lose[i]=0;
Node* p=cat[i];
Node* q;
while(p!=NULL){
q=p;
p=p->next;
delete q;
}
cat[i]=NULL;
}
int sp1,sp2;
for(int i=1;i<=M;i++){//初始化胜负关系
cin>>sp1>>sp2;
lose[sp2]++;
addEdge(sp1,sp2);
}
for(int i=1;i<=N;i++){
if(!lose[i])heap.push(i);
}
while(!heap.empty()){//拓扑排序
int theCat=heap.top();
heap.pop();
win[++cot]=theCat;
Node* p=cat[theCat];
while(p!=NULL){
lose[p->v]--;
if(lose[p->v]==0)heap.push(p->v);
p=p->next;
}
}
for(int i=1;i<=cot;i++){
if(i!=1)cout<<" ";
cout<<win[i];
}
cout<<endl;
}
}
C - 班长竞选
题意
大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?
思路
本题使用了强连通分量的知识。
意见传递可以看做图的连通关系,显然在一整个强连通分量中每个成员得到的肯定意见为分量中总人数减去自己。除此之外,并非强连通的分量之间依然有意见传递关系,因而最终每个人得到的意见(票数)为自己所在分量与通向自己所在分量的所有连通分量内的节点数-1。
利用Kosaraju算法,通过两次dfs,先生成逆后序序列,后完成了对原图中强连通分量的区分和染色。然后是对原图的缩点。构建一张新图来储存缩点的结果,这里再次使用dfs:用两个数组reachCC1、reachCC2来记录访问过程;从任意一点开始,在reachCC1中标记该点所处的SCC为已访问;在搜索过程中,若遇到同SCC内的节点,继续常规的dfs,若遇到属于不同SCC的节点,则在缩点图中反向添一条边连接两个SCC代表的节点,此处不再继续dfs,并在reachCC2中标记该SCC为已访问,确保下次访问时不再添边以免重复;dfs完成后,来到原图中的下一个不属于reachCC1中已标记的SCC的节点,重置reachCC2,再一次进入dfs。reachCC1中全部SCC被访问,缩点完成。
因为在缩点建图的过程中添加的是反向边,观察有向图的传递关系,易知传递意见最多的SCC必出现在入度为零的SCC中。在上文缩点的过程中预先如B题设置入度标记,可以只搜索比较入度为零的SCC,最终得到最大票数的值。根据输出格式,将所有属于票数等于最大票数SCC的节点按序输出即可。
Debug
提交遇到了两次TLE:
第一次,同题一,链表卡TLE,改用链式前向星;
修改后,第二次,因为使用memset重置空间,而当数据量不大时memset比for要多做很多多余的填充,导致了超时;
WA:
(1)输出少了冒号和最大票数之间的空格:以后不仅要看题面的输出描述,还要再观察一下样例输出的格式,看有无疏漏和理解错误;
(2)测试样例数据,没有发现逆后序序列有错;自己出小数据发现逆后续不对,修改后答案恰好没暴露问题,逆后序还是不对;
花了一定时间重新理解逆后序序列的表示而非想当然,终于书写正确;
(3)依然wa后测试多组数据,无法发现问题。后来意识到问题所在:自己出的多组数据,每组数据都是上一组改几条边,重置数组遗漏的地方被掩盖了,重新出差别较大的测试数据,得以修改。这里漏掉了缩点图入度的重置,影响到入度为0的分支判断。出数据不可偷懒。
代码
#include<iostream>
#include<string.h>
#include<stack>
#define maxN 100000
#define maxM 1000000
using namespace std;
int T,N,M;
int CC[maxN],vis[maxN];//CC:每个点所属连通分量;vis:访问标记
int d[maxN],f[maxN],rf[maxN];//前序、后序、逆后续
int dcnt,fcnt,rfcnt,cotCC;//前序计数、后序计数、连通分量计数
int numCC[maxN],VOTE,voteCC[maxN];//每个连通分量节点数、投票传递辅助变量、每个连通分量最终总票数
int inCC[maxN],vis2[maxN],reachCC1[maxN],reachCC2[maxN]; //入度非零标记,缩点图访问标记;缩点过程中:大循环到达标记,小循环到达标记
int head1[maxN],head2[maxN],head3[maxN];
struct edge{
int to,next;
}edges1[maxM],edges2[maxM],edges3[maxM];
int cotE1,cotE2,cotE3;
void addEdge(int u,int v){//为原图和反图加边
cotE1++;
edges1[cotE1].next=head1[u];
edges1[cotE1].to=v;
head1[u]=cotE1;
cotE2++;
edges2[cotE2].next=head2[v];
edges2[cotE2].to=u;
head2[v]=cotE2;
}
void addEdge2(int u,int v){
cotE3++;
edges3[cotE3].next=head3[u];
edges3[cotE3].to=v;
head3[u]=cotE3;
}
stack<int> S;
void dfs(int x){//前、后序序列深搜
d[x]=dcnt++;
vis[x]=1;
for(int i=head1[x];i>0;i=edges1[i].next){
if(!vis[edges1[i].to])dfs(edges1[i].to);
}
f[x]=fcnt++;
}
void Seq(){//生成逆后序序列
dcnt=fcnt=rfcnt=0;
while(!S.empty())S.pop();
for(int k=0;k<N;k++)vis[k]=0;
for(int i=0;i<N;i++)
if(!vis[i])dfs(i);
for(int i=0;i<N;i++){
rf[N-1-f[i]]=i;
}
}
void dfs2(int x){//逆后续深搜
CC[x]=cotCC;
numCC[cotCC]++;
for(int i=head2[x];i>0;i=edges2[i].next){
if(!CC[edges2[i].to])dfs2(edges2[i].to);
}
}
void dye(){//染色
for(int k=0;k<N;k++)CC[k]=0;
for(int k=1;k<=cotCC;k++)numCC[k]=0;
cotCC=0;
for(int i=0;i<N;i++){
if(!CC[rf[i]]){
++cotCC;
dfs2(rf[i]);
}
}
}
void dfs3(int x){//缩点过程深搜
vis[x]=1;
for(int i=head1[x];i>0;i=edges1[i].next){
int v=edges1[i].to;
if(!vis[v]){
if(CC[v]!=CC[x]){
if(!reachCC2[CC[v]]){
reachCC2[CC[v]]=1;
addEdge2(CC[v],CC[x]);
inCC[CC[x]]=1;
}
}else{
dfs3(v);
}
}
}
}
void dfs4(int x){//投票深搜
vis2[x]=1;
for(int i=head3[x];i>0;i=edges3[i].next){
if(!vis2[edges3[i].to]){
VOTE+=numCC[edges3[i].to];
dfs4(edges3[i].to);
}
}
}
void vote(int T){//投票
for(int k=1;k<=cotCC;k++)reachCC1[k]=inCC[k]=0;
for(int i=0;i<N;i++){//绘制缩点图
if(!reachCC1[CC[i]]){
reachCC1[CC[i]]=1;
for(int k=1;k<=cotCC;k++)reachCC2[k]=0;
for(int k=0;k<N;k++)vis[k]=0;
reachCC2[CC[i]]=1;
dfs3(i);
}
}
for(int i=1;i<=cotCC;i++){//投票
if(!inCC[i]){
for(int k=1;k<=cotCC;k++)vis2[k]=0;
VOTE=numCC[i];
VOTE--;
dfs4(i);
voteCC[i]=VOTE;
}
}
int max=0;
for(int i=1;i<=cotCC;i++){
if(!inCC[i]&&voteCC[i]>max)max=voteCC[i];
}
cout<<"Case "<<T<<": "<<max<<endl;
bool jdg=0;
for(int i=0;i<N;i++){
if(!inCC[CC[i]]&&voteCC[CC[i]]==max){
if(!jdg)jdg=1;
else cout<<" ";
cout<<i;
}
}
cout<<endl;
}
int main(){
ios::sync_with_stdio(false);
cin>>T;
for(int i=1;i<=T;i++){
cin>>N>>M;
cotE1=cotE2=cotE3=0;
for(int k=0;k<N;k++)head1[k]=head2[k]=head3[k]=0;
int A,B;
while(M--){
cin>>A>>B;
addEdge(A,B);
}
Seq();
dye();
vote(i);
}
}