A - 区间选点 II
给定一个数轴上的 n 个区间,要求在数轴上选取最少的点使得第 i 个区间 [ai, bi] 里至少有 ci 个点。
输入第一行一个整数 n 表示区间的个数,接下来的 n 行,每一行两个用空格隔开的整数 a,b 表示区间的左右端点。1 <= n <= 50000, 0 <= ai <= bi <= 50000 并且 1 <= ci <= bi - ai+1。
输出一个整数表示最少选取的点的个数。
思路:
- 首先我们需要将问题转化为一个差分约束系统。由于问题限定a b区间中点的个数至少为c ,联想到不等式的形式,可以有
sum[b] - sum[a] >= c
那么需要约定这里sum[x]
的含义:[0,x]中选取点的个数。可以想见最终的答案应该是sum[max(bi)]。如此根据输入便可以得到一组不等式。 - 仅仅以上不等式组是不够的:考虑每个点或者选或者不选,因此有
0<=sum[i]-sum[i-1]<=1
。 - 由于需要取最少的点,因此最终需要得到最小解,因此我们需要跑最长路(这一点考虑到松弛条件如果大于等于那么取等于,从而为最小解)另外,由于存在负边,我们需要使用SPFA。
- 另外需要注意数据的范围:左边界可以取到0,意味着有sum[0]-sum[-1]的存在,并且sum[-1]=0。这也应该成为跑SPFA的起点。但是显然-1没办法映射到下标,因此需要将区间整体向右平移一个单位,最终答案不会发生变化,而且此时sum[0]=0,从0开始跑SPFA。
实现:
#include<iostream>
#include<stdio.h>
#include <queue>
#include <vector>
#include<string.h>
using namespace std;
const int inf = 1e8;
const int N = 50010;
const int M = 200000;
int n, dis[N];
bool vis[N];
int head[N], tot;
struct Edge{
int to, next, w;
}e[M];
void add_edge(int x,int y,int z)
{
e[++tot].to=y;
e[tot].w=z;
e[tot].next=head[x];
head[x]=tot;
}
void spfa(int s) {
std::queue<int> q;
for(int i = 0; i <= 50005; ++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!=-1; i = e[i].next) {
int v = e[i].to;
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()
{
tot=0;
int a,b,c,r;
r = -1;
cin>>n;
memset(head,-1,sizeof(head));
for(int i=0;i<n;++i)
{
scanf("%d%d%d",&a,&b,&c);
r=max(b,r);
add_edge(a,b+1,c);
}
r++;
for(int i=1;i<=r;++i)
{
add_edge(i-1,i,0);
add_edge(i,i-1,-1);
}
spfa(0);
cout<<dis[r];
return 0;
}
反思:
- 错误1: 没有关注边的数量。尽管区间右边界限制了点的个数,但是边的个数需要另外计算,简单的排列组合问题,粗略估算大概是50000 * 50000 * 1/2。
- 错误2: 直接套用模板,没有细看代码。给出的最长路模板中,对于
dis vis
数组的初始化是从0到n,但是在main函数中n变成了区间的个数,显然不匹配,需要修改。
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 的猫猫。
给出一个符合要求的排名。输出时猫猫的编号之间有空格,最后一名后面没有空格!
其他说明:符合条件的排名可能不是唯一的,此时要求输出时编号小的队伍在前;输入数据保证是正确的,即输入数据确保一定能有一个符合要求的排名。
思路:
- 由于我们知道每场比赛的结果,因此可以得到一个有向图。计算名次序列等价于进行拓扑排序。需要注意的是要求字典序最小的名次序列,因此我们需要使用优先队列:同样入度为0的点,我们优先取出编号较小的点。
- 图的存储:考虑到我们需要计算入度,因此直接使用vector+结构体进行存储即可。
- 拓扑排序的算法:
- 遍历图中各点,计算其入度。对于入度为0的点,加入优先级队列。
- 当队列不为空时,挑出队首元素,将其加入答案队列。遍历其能到达的点,其入度减1。若为0,则加入队列。
- 由于保证数据正确,因此不需要判断最后的点的数目。
实现:
#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
#include<string.h>
using namespace std;
const int MAXN = 510;
struct edge
{
int u,v;
};
vector<edge> G[MAXN];
void add_edge(int u,int v)
{
G[u].push_back({u,v});
}
int N,M,P1,P2;
int in_deg[MAXN]={0};
vector<int> ans;
bool toposort()
{
for(int i=1;i<=N;++i)
{
vector<edge>::iterator ia= G[i].begin();
while(ia!=G[i].end())
{
in_deg[(*ia).v]++;
ia++;
}
}
priority_queue<int, vector<int>,greater<int> > q;
for(int i=1;i<=N;++i)
if(in_deg[i]==0)q.push(i);
while(!q.empty())
{
int x = q.top();
q.pop();
//cout<<"TEST: "<<x<<endl;
ans.push_back(x);
vector<edge>::iterator ia = G[x].begin();
while(ia!=G[x].end())
{
in_deg[(*ia).v]--;
if(in_deg[(*ia).v]==0)q.push((*ia).v);
ia++;
}
}
return true;
}
int main()
{
ios::sync_with_stdio(false);
while(cin>>N>>M)
{
for(int i=0;i<M;++i)
{
cin>>P1>>P2;
add_edge(P1,P2);
}
ans.clear();
memset(in_deg,0,sizeof in_deg);
toposort();
vector<int>::iterator ia=ans.begin();
if(ia!=ans.end())
{
cout<<*ia;
ia++;
}
while(ia!=ans.end())
{
cout<<" "<<*ia;
ia++;
}
cout<<endl;
for(int i=1;i<=N;++i)G[i].clear();
}
return 0;
}
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开始,紧跟着是最高的票数。接下来一行输出得票最多的同学的编号,用空格隔开,不忽略行末空格!
思路:
-
由于是有向无权图,因此直接使用
vector<int>
存储即可。注意需要建立两个图,原图和反图。 -
利用Kosaraju算法得到所有的SCC。此时的信息包括:每个点所属的SCC。据此进行缩点。为了后续的遍历,我们考虑计算每个SCC所包含的点的个数。遍历一遍即可。
-
注意在缩点的过程中需要将边反向。由于我们最终的答案存在于出度为0的SCC,因此反向之后寻找入度为0的点,从该点开始进行DFS,从而其到达的SCC即为答案的一部分。(缩点得到了一个新的图,此时图中可能有重复的边,不过并没有影响,因为DFS过程中会进行vis的判断,并且入度为0那么不存在入边。)为此我们考虑需要一个数组,下标表示SCC的编号,内容表示其中点的答案。初始值即为包含点的数目-1。
-
在完成DFS之后,我们得到了所有可能为答案的SCC对应答案,因此挑出其中的max,之后根据SCC编号输出其中的点的编号。注意可能会有多个SCC答案均为max,需要全部输出。
-
注意每个点的答案分成两部分:
- 其所属的SCC中点的数目-1
- 另外的SUM(SCCi中点的数目),其中SCCi可以到达该SCC。
因此最后答案一定存在于出度为0的SCC中。(否则,假设某SCC出度不为0,答案在该SCC中,那么其到达的SCC中点的答案一定大于它,矛盾)
实现:
#include<iostream>
#include<vector>
#include<string.h>
using namespace std;
const int N = 5010;
vector<int> G1[N],G2[N],G3[N],res[N];
int n,c[N],dfn[N],vis[N],dcnt,scnt;
//c -- 表示某点的SCC编号
//dfn -- DFS后序列中的点
//dcnt -- DFS序计数 scnt -- SCC计数
void dfs1(int x)
{
vis[x] = 1;
for(vector<int>::iterator y=G1[x].begin();y!=G1[x].end();++y)
if(!vis[*y])dfs1(*y);
dfn[dcnt++] = x;
}
void dfs2(int x)
{
c[x] = scnt;
for(vector<int>::iterator y=G2[x].begin();y!=G2[x].end();++y)
if(!c[*y])dfs2(*y);
}
void kosaraju()
{
dcnt = scnt = 0;
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
for(int i=0;i<n;++i)//注意点的编号从0开始
if(!vis[i])dfs1(i);
for(int i=n-1;i>=0;--i)
if(!c[dfn[i]])
{
++scnt;
dfs2(dfn[i]);
}
}
int num_point[N],in_deg[N],ans_scc[N];//每个SCC包含点的个数
int start,ans;
void dfs3(int x)
{
vis[x]=1;
for(vector<int>::iterator y=G3[x].begin();y!=G3[x].end();++y)
{
if(!vis[*y])
{
ans_scc[start]+=num_point[*y];
dfs3(*y);
}
}
}
void connect()
{
for(int i=0;i<n;++i)
{
num_point[c[i]]++;
}
for(int i=0;i<n;++i)
{
for(vector<int>::iterator y=G1[i].begin();y!=G1[i].end();++y)
{
if(c[i] != c[*y])
{
G3[c[*y]].push_back(c[i]);
in_deg[c[i]]++;
}
}
}
memset(vis,0,sizeof(vis));
for(int i=1;i<=scnt;++i)
{//此处i表示SCC的编号
ans_scc[i]=num_point[i]-1;
if(in_deg[i]==0)
{
start=i;
dfs3(start);
memset(vis,0,sizeof(vis));
}
}
}
void init()
{
for(int i=0;i<n;++i)
{
G1[i].clear();
G2[i].clear();
G3[i].clear();
}
memset(num_point,0,sizeof(num_point));
memset(in_deg,0,sizeof(in_deg));
memset(ans_scc,0,sizeof(ans_scc));
}
void out(int x)
{
int temp=0;
ans=0;
for(int i=1;i<=scnt;++i)
{//考虑到scnt从1开始
if(ans_scc[i] > temp)
{//是否存在多个相同的答案
temp = ans_scc[i];
ans = i;
}
}
int r[N];
int index=0;
for(int i=0;i<n;++i)
if(ans_scc[c[i]]==ans_scc[ans])
r[index++]=i;
cout<<"Case "<<x<<": "<<ans_scc[ans]<<endl;
for(int i=0;i<index-1;++i)
cout<<r[i]<<" ";
cout<<r[index-1]<<endl;
}
int main()
{
ios::sync_with_stdio(false);
int T,M,A,B;
cin>>T;
for(int i=1;i<=T;++i)
{
cin>>n>>M;
init()
for(int j=1;j<=M;++j)
{
cin>>A>>B;
G1[A].push_back(B);
G2[B].push_back(A);
}
kosaraju();
connect();
out(i);
}
}
反思:
- 错误1:dfs3过程中应该是
+= num_point[*y]
而非num_point[c[*y]]