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
解析
类似的题曾经在前面通过贪心的做法实现过,不过这里要求要使用差分约束的方法。
所谓差分约束,就是通过一组固定形式的不等式组来约束变量间的关系,其泛型为:𝑥1 − 𝑥2 ≤ 𝑐 ,c为常数。这类似于解不等式组操作。
对于泛型的不等式约束,我们通过移项,可以将其变为 𝑥1 ≤ 𝑐 + 𝑥2, 将c视为图中(链式前向星描述)的 w(i, j),x1视为dis [i] , x2视为dis [j],原式变为了 𝑑𝑖𝑠[𝑖] ≤ 𝑑𝑖𝑠[𝑗] + 𝑤(𝑖, 𝑗),与最短路的松弛操作类似。这样,我们把xi视作图中一个节点,对每个约束,视为从节点 i 到节点 j 连了一条长度为c的有向边(注意这里是有向边!),这样问题就变成了上周讲过的最短路问题。
当图中存在负环时(本题),用SPFA算法解决。
对本题,令sum [i] 为数轴上[ 0, i ] 之间选点的个数,其每个区间 [ ai, bi ] 满足约束条件如下:
𝑠𝑢𝑚[𝑏i] − 𝑠𝑢𝑚[𝑎i − 1] ≥ 𝑐i
转化成泛型,跑一遍最长路即可。
代码
#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int n = 50005;
const int m = 1000005;
const int inf = 5e8;
int N = 0, tot = 0, ans = 0;
int head[n], inq[n], dis[n];
queue<int> q;
struct edge{
int to, next, w;
}edges[m];
void add(int x, int y, int w){
edges[++tot].to = y;
edges[tot].next = head[x];
edges[tot].w = w;
head[x] = tot;
}
void SPFA(int s){
for(int i = 1 ; i <= ans ; ++i)
{
dis[i] = -inf;
inq[i] = 0;
}
dis[s] = 0;
inq[s] = 1;
q.push(s);
while(!q.empty())
{
int u = q.front();
q.pop();
inq[u] = 0;
for(int i = head[u] ; i != 0 ; i = edges[i].next)
{
int v = edges[i].to;
if(dis[v] < dis[u] + edges[i].w)
{
dis[v] = dis[u] + edges[i].w;
if(!inq[v])
{
q.push(v);
inq[v] = 1;
}
}
}
}
}
int main(){
int N;
cin >> N;
for(int i = 0 ; i < N ; i++)
{
int u, v, w;
cin >> u >> v >> w;
add(u, v + 1, w);
ans = max(ans, v + 1);
}
for(int i = 1 ; i <= ans ; i++)
{
add(i - 1, i, 0);
add(i, i - 1, -1);
}
SPFA(0);
cout << dis[ans] << endl;
return 0;
}
回顾
这题刚开始补充数据超时了,原因是直接套了上次C题的板子,好多没用的数组初始化没有删掉,事实告诉我们不要偷懒…
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
解析
拓扑排序问题。本题实质上就是给图中的节点(猫咪)排序。
拓扑排序的方法与求最小生成树的方法很相似,都是通过队列来实现。因为拓扑排序后的起点一定入度为0(可以通过反证法证明),所以先挑出虽有入度为零的点,将其入队,之后依次出队,遍历出队元素可到达的点,然后判断这些点入度减一,为0且未到达过则入队。重复这个过程至队列空,即可得到结果。
注意拓扑排序的结果不唯一。
代码
#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 50005;
const int M = 250005;
int tot = 0;
int head[N], inq[N], dis[N];
priority_queue<int, vector<int>, greater<int> > q;
vector<int> a;
int in_deg[510];
struct edge{
int to, next, w;
}edges[M];
void add(int x, int y, int w){
edges[++tot].to = y;
edges[tot].next = head[x];
edges[tot].w = w;
head[x] = tot;
}
void toposort(int n)
{
a.clear();
while(!q.empty())
q.pop();
for(int i = 1 ; i <= n ; i++)
if(in_deg[i] == 0)
q.push(i);
while(!q.empty())
{
int u = q.top();
q.pop();
a.push_back(u);
for(int i = head[u] ; i != 0 ; i = edges[i].next)
{
int v = edges[i].to;
if(--in_deg[v] == 0)
q.push(v);
}
}
}
int main()
{
int n, m;
while(cin >> n >> m)
{
tot = 0;
for(int i = 0 ; i <= n ; i++)
{
head[i] = 0;
in_deg[i] = 0;
}
for(int i = 0 ; i < m ; i++)
{
int p1, p2;
cin >> p1 >> p2;
add(p1, p2, 1);
in_deg[p2]++;
}
toposort(n);
for(int i = 0 ; i < a.size() - 1 ; i++)
cout << a[i] << " ";
cout << a[a.size()-1] << endl;
}
}
回顾
拓扑排序因为有之前的算法相印证,所以相对好理解。结合PPT中给出的代码,很容易就解决了这个问题。
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
解析
这道题综合性很强,思考起来也很复杂。构建一个图来解决它。
首先根据题意我们能够想到,在这个有向图中,如果一个人得到了另一个人的支持,那么它也一定得到了和和那个人处在同一个SCC(强连通分量)中的人的支持。如此,我们把图分成几个连通分量。
寻找连通分量,我们通过Kosaraju算法来解决。算法的步骤为,第一遍dfs1确定原图的逆后序序列;之后第二遍dfs2,根据逆后序序列依次遍历每个点,将到达的点标记,每次由起点遍历到的点即为一个联通分量(vis数组标记过的已到达的点将不再计算),所属的SCC由c数组记录。算法成立的原理是当图中的所有边反向时,强连通分量不受影响。
上面说到,如果一个人得到了另一个人的支持,那么它也一定得到了和和那个人处在同一个SCC(强连通分量)中的人的支持。那么,我们把每个强连通分量替换成一个点,将强连通分量间的连通关系视为点与点之间的连通关系。如此操作后,对于每个点,其支持人数分为 两部分,(令 SCC[i] 表示第 i 个 SCC 中点的个数),第一部分为当前 SCC 中的点,ans += SCC[i] – 1(去除自己),第二部分为其它 SCC 中的点 SUM ( SCC[j] ),其中 j 可到达 i。
如此,我们便求得了答案。
代码
#include<iostream>
#include<string.h>
#include<algorithm>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
vector<int> G1[5005], G2[5005], G3[5005];
int n, c[5005], dfn[5005], vis[5005], dcnt, scnt, indeg[5005];
struct point
{
vector<int> v;
}P[5005];
void dfs1(int x)
{
vis[x] = 1;
for(int i = 0 ; i < G1[x].size() ; i++)
if(!vis[G1[x][i]])
dfs1(G1[x][i]);
dfn[dcnt++] = x;
}
void dfs2(int x)
{
c[x] = scnt;
for(int i = 0 ; i < G2[x].size() ; i++)
if(!c[G2[x][i]])
dfs2(G2[x][i]);
}
int dfs3(int x)
{
vis[x] = 1;
int y = P[x].v.size();
for(int i = 0 ; i < G3[x].size() ; i++)
if(!vis[G3[x][i]])
y += dfs3(G3[x][i]);
return y;
}
void kosaraju()
{
dcnt = scnt = 0;
memset(c, 0, sizeof c);
memset(vis, 0, sizeof vis);
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 suodian(){
for(int i =1 ; i <= scnt ; i++)
{
G3[i].clear();
P[i].v.clear();
indeg[i] = 0;
}
for(int i = 0 ; i < n ; i++)
{
P[c[i]].v.push_back(i);
for(int j = 0 ; j < G1[i].size() ; j++)
{
if(c[i] == c[G1[i][j]])
continue;
else
{
G3[c[G1[i][j]]].push_back(c[i]);
indeg[c[i]]++;
}
}
}
}
void SET(){
for(int i = 1 ; i <= scnt ; i++)
{
sort(G3[i].begin(), G3[i].end());
G3[i].erase(unique(G3[i].begin(), G3[i].end()), G3[i].end());
}
}
int tag = 1;
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--)
{
int m;
cin >> n >> m;
for(int i = 0 ; i < n ; i++)
{
G1[i].clear();
G2[i].clear();
}
while(m--)
{
int a, b;
cin >> a >> b;
G1[a].push_back(b);
G2[b].push_back(a);
}
//求SCC
kosaraju();
//缩点
suodian();
//去重
SET();
int sum[scnt + 1] = {0};
int ans[5005] = {0};
int Max = 0;
for(int i = 1 ; i <= scnt ; i++)
{
if(!indeg[i])
{
for(int j = 1 ; j <= scnt ; j++)
vis[j] = 0;
sum[i] = dfs3(i) - 1;
if(Max < sum[i])
Max = sum[i];
}
}
int num = 0;
for(int i = 1 ; i <= scnt ; i++)
{
if(sum[i] == Max)
{
for(int j = 0 ; j < P[i].v.size() ; j++)
{
ans[num] = P[i].v[j];
num++;
}
}
}
sort(ans, ans + num);
cout << "Case " << tag << ": " << Max << endl;
tag++;
for(int i = 1 ; i < num ; i++)
cout << ans[i - 1] << " ";
cout << ans[num - 1] << endl;
}
}
回顾
这道题下课后听了一遍回放还是能理解的,可是实际操作时代码部分难度挺大的,瑟瑟发抖。虽然理解了缩点的操作,但是操作起来还是出了很多问题,最后十分艰难才解决。菜鸡瑟瑟发抖。
另外这是第一次因为没有加 ios::sync_with_stdio(false) 而导致超时的。这个点学长在第一节课说到过,但是没太在意,没想到它在某些情况下对时间的影响如此之大。