A - 区间选点 II
问题描述
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
(使用差分约束系统的解法解决这道题)
Input
输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。
Output
输出一个整数表示最少选取的点的个数
Sample Input
5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1
Sample Output
6
解题思路
- 求解差分约束系统,可转化为图论中单源最短路问题
- 对于差分约束中的每一个不等式约束 x − y ≤ c 都可以移项变形为 x≤y+c,可以使用最短路的松弛操作。
- 设sum[i]表示区间[0,i]上点的个数,则[a,b]中至少有c个点可转化为sum[b]-sum[a-1]>=c。
- 使用SPFA,对点u和它的临接点v,如果满足sum[v]<sum[u]+w(u,v)则进行松弛。输出最大点对应的sum即可。
- 为保证sum有意义,需要对i(1到MAX)加入(i-1,i,0)与(i,i-1,-1).
完整代码
#include <iostream>
#include<cstring>
#include <queue>
#include<vector>
using namespace std;
struct Edge{
int to;
int next;
int weight;
}e[200000];
int Max=0;
int reach[50010],sum[50010];
int head[50010],tot;
void add(int i,int t,int w)
{
e[++tot].to=t;
e[tot].weight=w;
e[tot].next=head[i];
head[i]=tot;
}
void spfa(int s)
{
queue<int> q;
while(!q.empty())
q.pop();
for(int i=0;i<=Max;i++)
{ sum[i]=-10;
reach[i]=0;}
q.push(s);
sum[s]=0;
while(!q.empty())
{
int u=q.front();
q.pop();
reach[u]=0;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(sum[v]<sum[u]+e[i].weight)
{
sum[v]=sum[u]+e[i].weight;
if(reach[v]==0)
{
q.push(v);
reach[v]=1;
}
}}
}
}
int main() {
int n;
tot=0;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int u,v,w;
scanf("%d %d %d",&u,&v,&w);
add(u,v+1,w);
Max=max(Max,v+1);
}
for(int i=1;i<=Max;i++)
{ add(i-1,i,0);add(i,i-1,-1);}
spfa(0);
printf("%d",sum[Max]);
}
B - 猫猫向前冲
问题描述
众所周知, TT 是一位重度爱猫人士,他有一只神奇的魔法猫。
有一天,TT 在 B 站上观看猫猫的比赛。一共有 N 只猫猫,编号依次为1,2,3,…,N进行比赛。比赛结束后,Up 主会为所有的猫猫从前到后依次排名并发放爱吃的小鱼干。不幸的是,此时 TT 的电子设备遭到了宇宙射线的降智打击,一下子都连不上网了,自然也看不到最后的颁奖典礼。
不幸中的万幸,TT 的魔法猫将每场比赛的结果都记录了下来,现在他想编程序确定字典序最小的名次序列,请你帮帮他。
Input
输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示猫猫的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即编号为 P1 的猫猫赢了编号为 P2 的猫猫。
Output
给出一个符合要求的排名。输出时猫猫的编号之间有空格,最后一名后面没有空格!
其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。
Sample Input
4 3
1 2
2 3
4 3
Sample Output
1 2 4 3
解题思路
- 输赢结果可以使用有向无环图表示:P1 赢了 P2可转化为 P1 到 P2 有一条边。
- 最终的名次 P1 要在 P2 的前面,所以转化为拓扑排序,即排在后面的“输着”不会依赖前面的“赢者”。
- 求字典序最小的排名,可以使用优先级队列,每次取队首元素,利用Kahn算法处理。
- Kahn:将入度为 0 的点组成一个集合 S , 每次从 S 里面取出一个顶点 u 放入 L , 然后遍历顶点 u 的所有边 (u, v) , 并删除之,并判断如果该边的另一个顶点 v,如果在移除这一条边后入度为 0 , 那么就将这个顶点放入集合 S 中。不断地重复取出顶点然后重复这个过程直到集合为空。检查图中是否存在任何边。如果有,那么这个图一定有环路,否者返回 L , L 中顺序就是拓扑排序的结果。
完整代码
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
vector<vector<int> > v;
int indegree[510];
int n,m;
priority_queue<int,vector<int>,greater<int> > q;//升序队列
queue<int > ans;
void solve()
{
for(int i=1;i<=n;i++)
if(indegree[i]==0)
q.push(i);//入度为0的点push进去
while(!q.empty())
{
int u=q.top();
q.pop();
ans.push(u);
for(int i=0;i<v[u].size();i++)
{
int w=v[u][i];
indegree[w]--;
if(indegree[w]==0) q.push(w);
}
}
if(ans.size()>0)
{cout<<ans.front();
ans.pop();}
while(!ans.empty())
{cout<<" "<<ans.front();
ans.pop();}
cout<<endl;
}
int main()
{
while(cin>>n>>m)
{ int a,b;
memset(indegree,0,sizeof(indegree));
v.clear();
v.resize(n+10);
for(int i=0;i<m;i++)
{
cin>>a>>b;
v[a].push_back(b);
indegree[b]++;
}
solve();
}
}
C - 班长竞选
问题描述
大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?
Input
本题有多组数据。第一行 T 表示数据组数。每组数据开始有两个整数 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下来有 M 行包含两个整数 A 和 B(A != B) 表示 A 认为 B 合适。
Output
对于每组数据,第一行输出 “Case x: ”,x 表示数据的编号,从1开始,紧跟着是最高的票数。 接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!
Sample Input
2
4 3
3 2
2 0
2 1
3 3
1 0
2 1
0 2
Sample Output
Case 1: 2
0 1
Case 2: 2
0 1 2
解题思路
- 本题考查了 Kosaraju算法的应用,通过找连通分量解决问题。
- 通过逆后序序列遍历缩点后,对第 i 个 SCC ,答案分为两部分,即 当前SCC中点的个数 – 1(去除自己)+ 能到达本SCC的其他SCC 中的点。
- 可以发现最后答案一定出现在出度为 0 的 SCC 中假设答案所在SCC出度不为0,则本SCC是本SCC指向的SCC答案的一部分,一定小于等于它。
- 将边反向,对每个入度为0 的点进行 dfs,计算能到达的点的SCC的总和,减一即可得到答案。
完整代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
vector<int> G[5010], G1[5010], G2[5010];//原图、反图、缩图
int f[5010],f1[5010],c[5010];//后序序列、逆后序序列
int visit[5010],SCC[5010],indeg[5010],ans[5010],Out[5010];
int cnt=0;
int fcnt=0;
int T,N,M,countt;
void dfs1(int x)//确定后序序列
{
visit[x]=1;
for(int i=0;i<G[x].size();i++)
if(visit[G[x][i]]==0)//没到过的点
dfs1(G[x][i]);
f[fcnt]=x;
// cout<<x<<" "<<f[x]<<endl;
fcnt++;
}
void nihou()
{
for(int i=0;i<fcnt;i++)
f1[N-1-i]=f[i];
}
void tu()
{
int A,B;
cin>>N>>M;
for(int i=0; i<N; i++)
G[i].clear(),G1[i].clear();
for(int i=0;i<M;i++)
{ cin>>A>>B;
G[A].push_back(B);//图
G1[B].push_back(A);//反图
}
}
void dfs2(int x)//逆序序列遍历,确定SCC(一次)
{
SCC[cnt]++;//连通分量的数目++
c[x]=cnt;//记录x所属的连通分量为cnt
visit[x]=1;
for(int i=0;i<G1[x].size();i++)
if(visit[G1[x][i]]==0)
dfs2(G1[x][i]);
}
void suotu()
{
for(int i=0;i<N;i++)
for(int l=0;l<G[i].size();l++)
{if(c[i]==c[G[i][l]])//看一下是不是一个连通分支的
continue;
G2[c[G[i][l]]].push_back(c[i]);
indeg[c[i]]++;}//反向插入边
//去重
for(int i=0;i<cnt;i++)
{
sort(G2[i].begin(),G2[i].end());
G2[i].erase(unique(G2[i].begin(),G2[i].end()),G2[i].end());
}
}
int dfs3(int x)//dfs能到他的所有SCC
{
visit[x]=1;
int answer=SCC[x];
for(int i=0;i<G2[x].size();i++)
if(!visit[G2[x][i]])
answer=answer+dfs3(G2[x][i]);
return answer;
}
void solve()
{
for(int i=0;i<cnt;i++)//第二部分
if(indeg[i]==0)//入度为0
{//cout<<i<<endl;
memset(visit,0,sizeof(visit));
ans[i]=dfs3(i)-1; //自己的点数-1
// cout<<"ans"<<ans[i]<<endl;
}
int Max=0;
int maxnumber;
for(int i=0;i<cnt;i++)
if(Max<ans[i])
Max=ans[i];
int con=0;
for(int i=0;i<N;i++)
if(ans[c[i]]==Max)
{Out[con]=i;
con++;
}
sort(Out,Out+con);
cout<<"Case "<<countt<<": "<<Max<<endl;
for(int i=0;i<con-1;i++)
cout<<Out[i]<<" ";
cout<<Out[con-1]<<endl;
}
int main()
{
ios::sync_with_stdio(false);
cin>>T;
for(countt=1;countt<=T;countt++)
{
tu();
cnt=0;
fcnt=0;
for(int i=0; i<5010; i++)
c[i]=0,visit[i]=0;
for(int i=0; i<N; i++)
if(visit[i]==0)
dfs1(i);
nihou();
memset(visit,0,sizeof(visit));
memset(SCC,0,sizeof(SCC));
for(int l=0;l<N;l++)
{if(visit[f1[l]]==0)
dfs2(f1[l]),cnt++;
}
for(int i=0;i<cnt;i++)
G2[i].clear(),indeg[i]=0,ans[i]=0,Out[i]=0;
//for(int i=0;i<N;i++)
// cout<<i<<" "<<c[i]<<endl;
suotu();
// for(int l=0;l<G2[0].size();l++)
// cout<<SCC[0]<<endl;
solve();
}
}