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
思路
使用差分约束系统首先需要构造不等式。题意为在区间
[
a
i
,
b
i
]
[a_i, b_i]
[ai,bi]中至少有
c
i
c_i
ci个点,这里用到了前缀和将区间转换为了差分形式,从开头到
b
i
b_i
bi 大区间的点数与开头到
a
i
−
1
a_i-1
ai−1小区间的点数之差即为区间
[
a
i
,
b
i
]
[a_i , b_i]
[ai,bi]中的点数(
s
u
m
[
i
]
sum[i]
sum[i]表示数轴上
[
0
,
i
]
[0,i]
[0,i]之间选点的个数)
s
u
m
[
b
i
]
−
s
u
m
[
a
i
−
1
]
≤
c
i
sum[ b_i] - sum[ a_i-1 ] \le c~i~
sum[bi]−sum[ai−1]≤c i
同时,区间内的一个点有选与不选两种情况,同样使用前缀和转换为差分的形式为
0
≤
s
u
m
[
i
]
−
s
u
m
[
i
−
1
]
≤
1
0 \le sum[ i ] - sum[ i-1 ] \le 1
0≤sum[i]−sum[i−1]≤1
求解最少选取的点数,也就是求解差分约束系统的最小解,对上面的两个不等式进行移项,得到
s
u
m
[
b
i
]
≥
s
u
m
[
a
i
−
1
]
+
c
i
sum[b_i] \ge sum[a_i-1] + c_i
sum[bi]≥sum[ai−1]+ci
s
u
m
[
i
]
≥
s
u
m
[
i
−
1
]
+
0
sum[i] \ge sum[i-1] + 0
sum[i]≥sum[i−1]+0
s
u
m
[
i
−
1
]
≥
s
u
m
[
i
]
+
(
−
1
)
sum[i-1] \ge sum[i] + (-1)
sum[i−1]≥sum[i]+(−1)
转换成了标准差分约束系统的形式
对于标准差分约束系统的一个不等式
a
−
b
≥
k
a-b\ge k
a−b≥k,移项为
a
≥
b
+
k
a\ge b+k
a≥b+k,可以理解为原点到a的距离大于原点到b的距离与
b
b
b到
a
a
a的距离之和,也就是相当于存在一条从
b
b
b到
a
a
a的长度为
k
k
k的有向边,对于每一个与
a
a
a相邻的
b
b
b,
d
i
s
[
a
]
≥
d
i
s
[
b
]
+
w
(
b
,
a
)
dis[a]\ge dis[b]+w(b,a)
dis[a]≥dis[b]+w(b,a) 根据spfa的松弛操作,每次大于等于都是取等于,因此保证了最后
d
i
s
[
a
]
dis[a]
dis[a]是满足要求的最小的(如果在某次松弛操作中给
d
i
s
[
a
]
dis[a]
dis[a]多加一点也是满足要求的,但明显每次取等于是最小的),这样松弛也相当于不断拉长
a
a
a与原点的距离,也就是取最长路。
最后找到所有区间中端点最靠右的右端点
b
b
b,
s
u
m
[
b
]
sum[b]
sum[b]就是所有区间最少选的点的个数。
由于仅仅需要
d
i
s
[
b
]
dis[b]
dis[b],原来spfa中所需的一些数组可以取消,需要注意的是,跑最长路时dis数组需要初始化为负无穷大,距离慢慢的变长,否则dis数组一直不变
void spfa(int s){
for(int i=0;i<=n;i++){
dis[i]=-inf;//最长路这里初始化为负无穷大
inq[i]=0;
}
while(q.size()) q.pop();
dis[s]=0;inq[s]=1;
q.push(s);
while(q.size()){
int u=q.front();q.pop();
inq[u]=0;
for(int i=head[u];i!=-1;i=edge[i].next){
int v=edge[i].to;
if(dis[v]<dis[u]+edge[i].weight){
dis[v]=dis[u]+edge[i].weight;
if(!inq[v]){
q.push(v);
inq[v]=1;
}
}
}
}
}
这里存储图使用了链式前向星的方式,需要注意的是链式前向星中的边不仅仅是输入的从a-1到b的边,还有相邻的两个点之间的边
for(int i=0;i<n;i++){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
maxb=max(maxb,b);
addedge(a-1,b,c);
}
for(int i=1;i<=maxb;i++){
addedge(i-1,i,0);
addedge(i,i-1,-1);
}
更容易被忽略的是链式前向星的边的条数不再是n,还要加上每相邻两个点之间的两条边,在定义数组的时候要注意
同时需要注意的是dis数组应该为long long 型的,如果是int型的可能溢出
const int nmax=50005;
Edge edge[nmax*3];//为了方便就直接乘3了
int head[nmax];//点的个数不变
代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <queue>
using namespace std;
struct Edge{
int from,to,next;
long long weight;
Edge(){from=-1;to=-1;weight=0;next=-1;}
Edge(int _from,int _to,long long _weight){
from=_from;
to=_to;
weight=_weight;
next=-1;
}
void show(){
cout<<"from:"<<from<<" to:"<<to<<" weight:"<<weight<<" next:"<<next<<endl;
}
};
const int nmax=50010;
const long long int inf=1e12;
Edge edge[nmax*4];
int head[nmax];
int ind,n;
void init(){
// for(int i=0;i<=3*n;i++) head[i]=-1;
memset(head,-1,sizeof head);
ind=0;
}
void addedge(int from,int to,long long weight){
edge[ind].from=from;
edge[ind].to=to;
edge[ind].weight=weight;
edge[ind].next=head[from];
head[from]=ind++;
}
int inq[nmax];
long long dis[nmax];
queue<int> q;
void spfa(int s){
for(int i=0;i<=n;i++){
dis[i]=-inf;//最长路这里初始化为负无穷大
inq[i]=0;
}
while(q.size()) q.pop();
dis[s]=0;
inq[s]=1;
q.push(s);
while(q.size()){
int u=q.front();q.pop();
inq[u]=0;
for(int i=head[u];i!=-1;i=edge[i].next){
int v=edge[i].to;
if(dis[v]<dis[u]+edge[i].weight){
dis[v]=dis[u]+edge[i].weight;
if(!inq[v]){
q.push(v);
inq[v]=1;
}
}
}
}
}
int main(){
scanf("%d",&n);
int maxb=-1,mina=1e5;
init();
for(int i=0;i<n;i++){
int a,b;
long long c;
scanf("%d%d%lld",&a,&b,&c);
addedge(a,b+1,c);
maxb=max(maxb,b+1);
mina=min(mina,a);
}
for(int i=mina;i<maxb;i++){
addedge(i,i+1,0);
addedge(i+1,i,-1);
}
spfa(mina);
cout<<dis[maxb]<<endl;
return 0;
}
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 3 4
思路
上周有一道题目与这道题的题意类似,只不过那道题目是求取闭包,得出任意两个点的连通关系,而这道题目是求拓扑排序,并且题目保证这个图是连通的。
拓扑排序很类似与bfs,从图中向队列中添加入度为0的点(入度的数组在添加边的同时建立),从队列中不断取出一个点,删除这个点相邻接(指向)的边,不断更新与这个点相邻接的点的入度,并且将入度为0的点再加入队列,过程与bfs只是在入队条件那里有些区别。如果要求出字典序最小的拓扑序,将队列换成优先队列(小根堆),保证每次从队列中弹出的是当前编号最小的点。
bool toposort(){
for(int i=1;i<=n;i++){
if(indeg[i]==0) q.push(i);
}
while(q.size()){
int temp=q.top();q.pop();
s.push_back(temp);
for(int i=head[temp];i!=-1;i=edge[i].next){
indeg[edge[i].to]--;
if(indeg[edge[i].to]==0)
q.push(edge[i].to);
}
head[temp]=-1;
}
return s.size()==n;
}
存储边同样使用了链式前向星,删除边时只需将head中的指针置为-1即表示删除。
代码
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <queue>
#include <vector>
using namespace std;
struct Edge{
int from,to,next;
Edge(){from=-1;to=-1;next=-1;}
Edge(int _from,int _to){
from=_from;
to=_to;
next=0;
}
void show(){
cout<<"from:"<<from<<" to:"<<to<<" next:"<<next<<endl;
}
};
const int nmax=500;
const int lenMax=nmax*nmax+5;
Edge edge[lenMax];
int head[lenMax],indeg[lenMax];
int cnt,m,n;
priority_queue<int,vector<int>,greater<int>> q;
vector<int> s;
queue<int> t;
void init(){
cnt=0;s.clear();
for(int i=0;i<lenMax;i++) head[i]=-1;
while(q.size()) q.pop();
}
void addedge(int from,int to){
edge[cnt].from=from;
edge[cnt].to=to;
edge[cnt].next=head[from];
head[from]=cnt++;
indeg[to]++;
}
void deledge(int from){
indeg[edge[head[from]].to]--;
head[from]=-1;
}
bool toposort(){
for(int i=1;i<=n;i++){
if(indeg[i]==0) q.push(i);
}
while(q.size()){
int temp=q.top();q.pop();
s.push_back(temp);
for(int i=head[temp];i!=-1;i=edge[i].next){
indeg[edge[i].to]--;
if(indeg[edge[i].to]==0)
q.push(edge[i].to);
}
head[temp]=-1;
}
return s.size()==n;
}
int main(){
while(~scanf("%d%d",&n,&m)){
init();
for(int i=0;i<m;i++){
int a,b;
scanf("%d%d",&a,&b);
addedge(a,b);
}
if(toposort()){
for(int i=0;i<n-1;i++)
cout<<s.at(i)<<" ";
cout<<s.at(n-1)<<endl;
}
}
return 0;
}
C-班长竞选
大学班级选班长,N个同学均可以发表意见若意见为A B则表示A认为B合适,意见具有传递性,即A认为B合适,B认为C合适,则A也认为C合适勤劳的TT收集了M条意见,想要知道最高票数,并给出一份候选人名单,即所有得票最多的同学,你能帮帮他吗?
Input
本题有多组数据。第一行T表示数据组数。每组数据开始有两个整数N和M( 2 ≤ n ≤ 5000 2\le n \le 5000 2≤n≤5000, 0 < m ≤ 30000 0 < m \le 30000 0<m≤30000),接下来有M行包含两个整数A和B( A ≠ B A \not =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
思路
如果两个人通过传递性可以达到相互认同,那么这两个人在同一个SCC中,因此首先要找到图中的SCC。
kosaraju算法
使用kosaraju算法,第一个dfs确定原图的逆后序序列,第二次dfs在反图中按照逆后序序列进行遍历,对不同的SCC进行染色。
void dfs1(int x){
vis[x]=true;
for(int i=0;i<g1[x].size();i++){
int temp=g1[x][i];
if(!vis[temp]) dfs1(temp);
}
d[dcnt++]=x;
}
void dfs2(int x){
c[x]=scnt;num[scnt]++;
for(int i=0;i<g2[x].size();i++){
int temp=g2[x][i];
if(!c[temp]) dfs2(temp);
}
}
void kosaraju(){
for(int i=0;i<n;i++){
if(!vis[i]) dfs1(i);
}
for(int i=n-1;i>=0;i--){
if(!c[d[i]]) ++scnt,dfs2(d[i]);
}
}
接下来对反图中的SCC进行缩点(原图和反图的SCC相同),将同一种染色的SCC看作是一个节点,重新建图
for(int i=0;i<n;i++){
for(int j=0;j<g2[i].size();j++){
int temp=g2[i][j];
if(c[i]==c[temp]) continue;//在同一个SCC中
g3[c[i]].push_back(c[temp]);
indeg[c[temp]]++;
}
}
缩点后,对于第i个SCC中的点来说,答案分为两个部分,一方面来自于自己SCC中的其他点,另一方面来自于其他可以到达第i个SCC的SCC,因此没有出边的SCC可能是最后的答案,通过对反图缩点重新建图,这样就把入边改变成了出边,将没有出边变成了没有入边,对入度为0的SCC进行dfs或bfs即可
这是大体思路,但是在具体的过程中有许多细节要注意
首先要注意同学的编号是从0开始的,也就是在dfs求逆后序序列时要注意索引的自增与赋值的顺序(先赋值,后自增)
void dfs1(int x){
vis[x]=true;
for(int i=0;i<g1[x].size();i++){
int temp=g1[x][i];
if(!vis[temp]) dfs1(temp);
}
d[dcnt++]=x;
}
其次在记录同学的编号时,使用小根堆来记录比较方便。
最后,原图和反图是可以用链式前向星来存储的,但是缩点图比较适合用vector的邻接链表来存储,点和边相对都少,而且是动态的,这时使用前向星就会占用较多空间。
代码
#include <iostream>
#include <stdio.h>
#include <vector>
#include <queue>
#include <string.h>
#include <algorithm>
using namespace std;
const int nmax=5005;
const int mmax=3e4+5;
int t,n,m,a,b;
vector<int> g1[nmax],g2[nmax],g3[nmax];
int d[nmax],c[nmax],indeg[nmax],num[nmax];
bool vis[nmax];
int dcnt,scnt,ans;
queue<int> q;
priority_queue<int,vector<int>,greater<int>> p;
void init(){
dcnt=0;scnt=0;ans=0;
memset(d,0,sizeof d);
memset(c,0,sizeof c);
memset(indeg,0,sizeof indeg);
memset(num,0,sizeof num);
memset(vis,false,sizeof vis);
for(int i=0;i<nmax;i++){
g1[i].clear();
g2[i].clear();
g3[i].clear();
}
}
void dfs1(int x){
vis[x]=true;
for(int i=0;i<g1[x].size();i++){
int temp=g1[x][i];
if(!vis[temp]) dfs1(temp);
}
d[dcnt++]=x;
}
void dfs2(int x){
c[x]=scnt;num[scnt]++;
for(int i=0;i<g2[x].size();i++){
int temp=g2[x][i];
if(!c[temp]) dfs2(temp);
}
}
void kosaraju(){
for(int i=0;i<n;i++){
if(!vis[i]) dfs1(i);
}
for(int i=n-1;i>=0;i--){
if(!c[d[i]]) ++scnt,dfs2(d[i]);
}
}
int bfs(int x){
memset(vis,false,sizeof vis);
int result=num[x];
queue<int> qu;
while(qu.size()) qu.pop();
qu.push(x);vis[x]=true;
while(qu.size()){
int i=qu.front();qu.pop();
for(int j=0;j<g3[i].size();j++){
if(vis[g3[i][j]]) continue;
qu.push(g3[i][j]);vis[g3[i][j]]=true;
result+=num[g3[i][j]];
}
}
return result;
}
int main(){
scanf("%d",&t);
for(int u=1;u<=t;u++){
init();
scanf("%d%d",&n,&m);
for(int i=0;i<m;i++){
scanf("%d%d",&a,&b);
g1[a].push_back(b);
g2[b].push_back(a);
}
kosaraju();
for(int i=0;i<n;i++){
for(int j=0;j<g2[i].size();j++){
int temp=g2[i][j];
if(c[i]==c[temp]) continue;//在同一个SCC中
g3[c[i]].push_back(c[temp]);
indeg[c[temp]]++;
}
}
while(q.size()) q.pop();
for(int i=1;i<=scnt;i++){
if(indeg[i]==0) q.push(i);
}
while(p.size()) p.pop();
int result=0;
vector<int> s;
while(q.size()){
int cluster=q.front();q.pop();
int temp=bfs(cluster);
if(result<temp){
s.clear();
s.push_back(cluster);
result=temp;
}
else if(result==temp)
s.push_back(cluster);
}//s中存储了集合的编号
while(p.size()) p.pop();
for(int i=0;i<s.size();i++){
int temp=s.at(i);
// cout<<"temp:"<<temp<<endl;
for(int j=0;j<n;j++){
if(c[j]==temp)
p.push(j);
}
}
printf("Case %d: %d\n",u,result-1);
while(p.size()!=1){
cout<<p.top()<<" ";p.pop();
}
cout<<p.top()<<endl;p.pop();
}
}