week8-淦
A:区间选点——(差分约束与spfa)
题目:
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点
使用差分约束系统的解法解决这道题
输入:
输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。
输出:
输出一个整数表示最少选取的点的个数
样例:
样例输入:
5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1
样例输出:
6
思路:
差分约束系统
- 一种特殊的n元一次不等式组,它包含n个变量以及m个约束条件。
- 每种约束条件是由其中的变量做差构成的,形如Xi-Xj<Ck,其中Ck是常数
- 我们要解决的问题是:求一组解x1=a1,x2=a2,…xn=an,使得所有的约束条件得到满足,否则判断出无解。
求解差分约束系统,都可以转化为图论中得单源最短路问题
对于差分约束中的每一个不等式约束Xi-Xj<Ck,都可以移项变形为Xi<=Ck+Xj.如果令Ck=w(i,j) dis[i]=Xi,dis[j]=Xj ,那么原式变为dis[i]<=dis[j]+w(i,j),与最短路中的松弛操作相似。
由于原式为Xi<=Ck+Xj,求解是按照等于求解,跑最短路出来的最大解,如果想要最小解即为Xi>=Ck+Xj,跑最长路。
构造不等式组:
记sum[i]表示数轴上[0,i]之间选点的个数
对于第i个区间[ai,bi]需要满足sum[bi]-sum[ai-1]>=ci
同时需要保证sum[i]是有意义的,
0<=sum[i]-sum[i-1]<=1;
这时就会出现0<=sum[1]-sum[0]<=1; 此时为了方便整体后移改成sum[bi+1]-sum[ai]>=ci ; (可以后移也是基于差分约束系统+d,-d结果不变)
代码:
#include<stdio.h>
#include<queue>
#include<iostream>
#include<string.h>
using namespace std;
#define maxn 50010
#define inf 1e9
struct edge{
int u,v,w,next;
};
edge E[5000100];
int head[maxn],tot;
int dis[maxn];
bool vis[maxn];
int n,r;
void add(int u,int v,int w)
{
E[++tot].u=u;
E[tot].v=v;
E[tot].w=w;
E[tot].next=head[u];
head[u]=tot;
}
void spfa(int s)
{
queue<int> q;
while(!q.empty()) q.pop();
for(int i=0;i<=r;i++){
dis[i]=-inf;
vis[i]=0;
}
q.push(s);
dis[s]=0;
vis[s]=1;
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=E[i].next)
{
int v=E[i].v;
if(dis[v]<dis[u]+E[i].w)
{
dis[v]=dis[u]+E[i].w;
if(!vis[v])
{
q.push(v);
vis[v]=1;
}
}
}
}
}
int main()
{
cin>>n;
int a,b,c;
tot=0;
memset(head,0,sizeof(head));
for(int i=0;i<n;i++)
{
cin>>a>>b>>c;
add(a,b+1,c);
r=max(r,b+1);
}
for(int i=1;i<=r;i++){
add(i-1,i,0);
add(i,i-1,-1);
}
spfa(0);
printf("%d\n",dis[r]);
return 0;
}
B:猫猫向前冲——(拓扑排序)
题目:
众所周知, TT 是一位重度爱猫人士,他有一只神奇的魔法猫。
有一天,TT 在 B 站上观看猫猫的比赛。一共有 N 只猫猫,编号依次为1,2,3,…,N进行比赛。比赛结束后,Up 主会为所有的猫猫从前到后依次排名并发放爱吃的小鱼干。不幸的是,此时 TT 的电子设备遭到了宇宙射线的降智打击,一下子都连不上网了,自然也看不到最后的颁奖典礼。
不幸中的万幸,TT 的魔法猫将每场比赛的结果都记录了下来,现在他想编程序确定字典序最小的名次序列,请你帮帮他。
输入:
输入有若干组,每组中的第一行为二个数N(1<=N<=500),M;其中N表示猫猫的个数,M表示接着有M行的输入数据。接下来的M行数据中,每行也有两个整数P1,P2表示即编号为 P1 的猫猫赢了编号为 P2 的猫猫。
输出:
给出一个符合要求的排名。输出时猫猫的编号之间有空格,最后一名后面没有空格!
其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。
样例:
样例输入:
4 3
1 2
2 3
4 3
样例输出:
1 2 4 3
思路:
这个分析题意比较容易看出来是一个拓扑排序的题目。
拓扑排序的基本步骤:
从入度为0的点加入到队列中,选取队列中的一个点,将 u 加入结果数组ans,并且 u 出发的线去掉,对应的v,Ru[u]–,同时将入度为0的v 加入到队列中;当ans 中的点是n 的时候,所有点加入完毕,输出结果。
如果是一般的拓扑排序用队列实现,结果可以有很多种,但是现在要求字典序最小,因此不能用一般的队列,而应该用优先级队列 。
总结:
这个题的思路是比较明确的,需要注意的是,这里数据结构的选取。
这里需要用到的就是获得从u->v 的边,这里用链式前向星就不是很合适了,找边的时候增加了困难。可以用的数据结构可以是vector 存储的二维数组,静态数组,链式存储。这里选用的比较方便的静态数组。
代码:
#include<stdio.h>
#include<queue>
#include<vector>
#include<iostream>
#include<string.h>
using namespace std;
#define maxn 550
int cat[maxn][maxn];
int R[maxn]; //记录入度
int n;
int vis[maxn];
void kahn()
{
priority_queue<int,vector<int>,greater<int>> q;
vector<int> ans;
while(!q.empty()) q.pop();
for(int i=1;i<=n;i++)
{
if(R[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u=q.top() ; //去掉以u 为起始点的边,边对应的终点度数--
q.pop();
ans.push_back(u);
if(ans.size()==n){
for(int i=0;i<n;i++){
if(i!=n-1) printf("%d ",ans[i]);
else printf("%d",ans[i]);
}
}
for(int i=1;i<=n;i++){
if(cat[u][i]==1){
R[i]--;
if(R[i]<=0){
q.push(i);
}
}
}
}
}
int main()
{
int m,a,b;
while(scanf("%d%d",&n,&m)!=EOF){
memset(cat,0,sizeof(cat));
memset(R,0,sizeof(R));
while(m--){
cin>>a>>b;
if(!cat[a][b]){
cat[a][b]=1; //a 打败 b
R[b]++;
}
}
kahn();
printf("\n");
}
}
C:班长竞选——(SCC 与缩点)
题目:
大学班级选班长,N 个同学均可以发表意见 若意见为 A B 则表示 A 认为 B 合适,意见具有传递性,即 A 认为 B 合适,B 认为 C 合适,则 A 也认为 C 合适 勤劳的 TT 收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?
输入:
本题有多组数据。第一行 T 表示数据组数。每组数据开始有两个整数 N 和 M (2 <= n <= 5000, 0 <m <= 30000),接下来有 M 行包含两个整数 A 和 B(A != B) 表示 A 认为 B 合适。
输出:
对于每组数据,第一行输出 “Case x: ”,x 表示数据的编号,从1开始,紧跟着是最高的票数。 接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!
样例:
样例输入:
2
4 3
3 2
2 0
2 1
3 3
1 0
2 1
0 2
样例输出:
Case 1: 2
0 1
Case 2: 2
0 1 2
思路:
这个题 的目标就是求出最高的票数和获得最高票数的人;
在一个强连通图中每个人获得的票数的就是连通图中的点-1(自身),其次就要考虑其他图连向该图的点。
这样分析后思路也会比较清晰:
(1)用Kosaraju 求出scc
(2)缩点,一个SCC作为一个点,连接SCC之间的线,得图G。此时存反图 fG。遍历点得到,每个 SCC中的详细的点。遍历边,然后·寻找跨越不同SCC 中的边。(需要注意的是,SCC之间的边可能重复,因此需要用vector 中的函数进行去重)
(3)经分析可以知道,最大的票数应该在G 中指向SCC 最多的,但是这样计算不方便,因此用反图fG, 从出度为0的SCC 遍历,获取此SCC所能到达的所有SCC ,统计票数。
总结:
总的体验就是步骤繁琐,但是好在每一步都比较清晰,debug 时每一步能达到的效果都是知道的,比较容易找错。这次给人的感受是数据结构根据需要选用。
查找最后结果有两种情况,SCC内点和其他SCC 中的点,但是这两种情况不是每个SCC 都存在的,因此,计算的时候也需要分成两部分,用an 记录其他SCC 指向该SCC 的点,本身SCC 中的点必然存在。
Koasaraju算法步骤:
(1)第一遍dfs 确定原图的逆后序序列
(2)第二遍dfs 在反图中按照逆后序序列进行遍历(每次由起点遍历到的点即构成一个SCC)
Koasaraju获得重要的内容:
(1)c[i]:顶点 i 所在的 SCC
(2)scnt :SCC的总数目
Koasaraju 模板代码:
void dfs1(int u)
{
vis[u]=1;
for(int i=0;i<a[u].size();i++){
if(!vis[a[u][i]]){
dfs1(a[u][i]);
}
}
dfn[dcnt]=u; //后序序列
dcnt++;
}
void dfs2(int u)
{
c[u]=scnt; //c[i] 顶点i 所在的scc编号 scnt 总scc的数量
for(int i=0;i<b[u].size();i++){
if(!c[b[u][i]]){
dfs2(b[u][i]);
}
}
}
void kosaraju()
{
dcnt=scnt=0;
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
memset(dfn,-1,sizeof(dfn));
for(int i=0;i<n;i++){
if(!vis[i]) dfs1(i);
}
for(int i=n-1;i>=0;i--){ //按照逆后序序列遍历
if(!c[dfn[i]]){ //当前点还不在任意一个SCC
++scnt; //出现新的SCC scnt++
dfs2(dfn[i]);
}
}
}
代码:
#include<stdio.h>
#include<vector>
#include<string.h>
#include<algorithm>
#include<iostream>
using namespace std;
#define N 5010
vector<int> a[N],b[N],G[N],G1[N];
int n,c[N],vis[N],dfn[N],dcnt,scnt,Ru[N],sum[N],anss[N];
int an;
struct edge{
int u,v;
};
edge E[30010]; //边集
void dfs1(int u)
{
vis[u]=1;
for(int i=0;i<a[u].size();i++){
if(!vis[a[u][i]]){
dfs1(a[u][i]);
}
}
dfn[dcnt]=u; //后序序列
dcnt++;
}
void dfs2(int u)
{
c[u]=scnt; //c[i] 顶点i 所在的scc编号 scnt 总scc的数量
for(int i=0;i<b[u].size();i++){
if(!c[b[u][i]]){
dfs2(b[u][i]);
}
}
}
void kosaraju()
{
dcnt=scnt=0;
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
memset(dfn,-1,sizeof(dfn));
for(int i=0;i<n;i++){
if(!vis[i]) dfs1(i); //
}
for(int i=n-1;i>=0;i--){
if(!c[dfn[i]]){
++scnt;
dfs2(dfn[i]);
}
}
}
void dfs3(int u) //所有u 能到达的点
{
vis[u]=1;
//an=G1[u].size(); //G1 SCC 内点 G缩点后的图 G[u][i] 从u能到达的点
for(int i=0;i<G[u].size();i++){
int v=G[u][i];
if(!vis[v]) {
vis[v]=1;
an=an+G1[v].size();
// cout<<v<<" "<<G1[v].size()<<" "<<an<<endl;
dfs3(v);
}
}
}
int main()
{
int T,m,A,B;
scanf("%d",&T);
for(int kk=1;kk<=T;kk++)
{
scanf("%d%d",&n,&m);
for(int i=0;i<=n+1;i++)
{
a[i].clear();
b[i].clear();
G1[i].clear();
G[i].clear();
}
int k=0;
for(int i=0;i<m;i++){
scanf("%d%d",&A,&B);
a[A].push_back(B); //原图
b[B].push_back(A); //反图
E[k].u=A;
E[k].v=B;
k++;
}
kosaraju(); //求SCC
//记录每个scc中的点的个数
for(int i=0;i<n;i++)
{
G1[c[i]].push_back(i); //第i 个点在scc
}
//cout<<G1[1].size()<<endl;
//遍历所有边,得scc 的新图
memset(Ru,0,sizeof(Ru));
for(int i=0;i<m;i++){
if(c[E[i].u]!=c[E[i].v]){ //连接两个scc的边 //加入到新图中 c[i] 是顶点,
G[c[E[i].v]].push_back(c[E[i].u]); //缩点后的反图
Ru[c[E[i].u]]++;
}
}
/*
cout<<"scnt : "<<scnt<<endl;
cout<<"反图 :" <<endl;
for(int i=1;i<=scnt;i++){
cout<<"sc: "<<i<<" ";
for(int j=0;j<G[i].size();j++){
cout<<G[i][j]<<" "; //
}
cout<<endl;
}
cout<<"SCC内点"<<endl;
for(int i=1;i<=scnt;i++){
cout<<"scn: "<<i<<": ";
for(int j=0;j<G1[i].size();j++){
cout<<G1[i][j]<<" ";
}
cout<<endl;
} */
//去重,可能有相同的边
for(int i=1;i<=scnt;i++)
{
sort(G[i].begin(),G[i].end());
G[i].erase(unique(G[i].begin(),G[i].end()),G[i].end());
}
// 求入度为0的新图的dfs
memset(sum,0,sizeof(sum));
bool flag=false;
for(int i=1;i<=scnt;i++)
{
an=0;
if(Ru[i]==0) //如果不存在入度为0的点
{
memset(vis,0,sizeof(vis)); //dfs 错误
dfs3(i);
}
sum[i]=an+G1[i].size()-1;
}
//求出最大值 所在scc-1 + dfs()
int cnt,ans=0;
for(int i=1;i<=scnt;i++)
{
// cout<<ans<<" "<<sum[i]<<endl;
if(ans<sum[i])
{
ans=sum[i]; //最高的票数
}
}
//求拥有最高票数的同学
k=0;
memset(anss,0,sizeof(anss));
for(int i=1;i<=scnt;i++)
{
if(sum[i]==ans)
{
//scc中所有的点都需要加入
for(int j=0;j<G1[i].size();j++)
{
anss[k]=G1[i][j];
k++;
}
}
}
sort(anss,anss+k);
//输出:最高的票数+同学编号
printf("Case %d: %d\n",kk,ans);
for(int i=0;i<k-1;i++){
printf("%d ",anss[i]);
}
printf("%d\n",anss[k-1]);
}
return 0;
}