2021.2.2 考试解题报告
2021.2.2 考试解题报告
得分
| 题目 | 实际得分 | 期望得分 | 知识点 |
|---|---|---|---|
| A 公交车 | \(30\) | \(60\) | 最短路,图论,剪枝 |
| B 灌溉 | \(100\) | \(100\) | 最小生成树 |
| C 决斗 | \(10\) | \(0\) | 博弈论,DAG |
A 公交车
这道题我数组越界直接嗝了,\(60\) --> \(30\)。
当我看到 :
对于 \(30%\) 的数据,\(n \leq 100\) ,\(k = 1\)。
对于另外 \(30%\) 的数据,\(k = 0\)。
直接测试点分治,先把 \(30\) 分拿到手,\(k = 0\) 时跑个最短路直接出答案,虽然最后也就得了这三十分。
考场上我对题目理解错了,我以为公交车站点和每个地点是重合的。
\(60\) \(pts\)
我们思考“公交车”的本质,实际上就是在同一线路上的两个点可以以 \(b_i\) 的代价到达。
那么有一个直接的想法就是枚举在同一线路上的任意两个点,然后在它们之间连边。这样会连出 \(O((\sum t_i)^2)\) 条边,但是 $\sum t_i $有 \(20\) 万,这个复杂度空间复杂度太高,只能拿到前面 \(60\) \(pts\)。
因为直接写挂了,所以没有 \(60\) 分代码。/cy。
\(100\) \(pts\)
其实我们可以对公交车建一个点,路线上的每个点连向这个新建的点,边权为 \(b_i\) ;新建的点也连向路线上的每一个点,边权为 \(0\) 。
这样图的点数为 \(n + k\) ,边数为 \(m + \sum t_i\) ,可以承受。
对了,做题一定要看数据范围, \(1 \leq b_i , t_i \leq 10 ^ 9\)。不开 \(long\) $long $ 见祖宗。
【Code】
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<set>
#include<algorithm>
#include<cmath>
#include<vector>
#define ll long long
#define ull unsigned long long
#define M 2000030
#define N 203000
#define INF 0x3f3f3f3f
using namespace std;
/*-----------------------------------*/
int n,m,k,cnt,s;
struct nod{
int id;
ll val;
inline bool operator < (const nod &id) const
{
return val > id.val;
}
};
struct node{
int next;
int to;
ll dis;
}e[M];
int head[N];
ll w[N];
int vis[N];
/*-----------------------------------*/
void add(int from,int to,int dis)
{
e[++ cnt].to = to;
e[cnt].dis = dis;
e[cnt].next = head[from];
head[from] = cnt;
return;
}
void dij(int x)
{
priority_queue <nod> qp;
memset(w,INF,sizeof w);
w[x] = 0ll;
qp.push((nod){x,w[x]});
while(!qp.empty()) {
nod cur = qp.top();
qp.pop();
int u = cur.id;
if(vis[u]) continue;
vis[u] = true;
for(int i = head[u];i;i = e[i].next) {
int y = e[i].to;
if(w[y] > w[u] + e[i].dis) {
w[y] = w[u] + e[i].dis;
qp.push((nod){y,w[y]});
}
}
}
return;
}
/*------------------------------------*/
int main()
{
// freopen("transprt1.in","r",stdin);
// freopen("transprt2.out","w",stdout);
scanf("%d%d%d%d",&n,&m,&k,&s);
for(int i = 1;i <= m;i ++) {
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);//连边
}
if(k == 0) {//当 k = 0 时,直接跑一遍最短路。
dij(s);
for(int i = 1;i <= n;i ++) printf("%lld ",w[i]);
return 0;
}
else {
for(int i = 1;i <= k;i ++) {
int mon,t,y;
scanf("%d%d",&mon,&t);
for(int j = 1;j <= t;j ++) {
scanf("%d",&y);
add(n + i,y,0);//新建一个节点,从 n + i --> y
add(y,n + i,mon);//从 y --> n + i;
}
}
dij(s);
for(int i = 1;i <= n;i ++) printf("%lld ",w[i]);
}
return 0;
}
B 灌溉
这是道原题...
loj 10066 新的开始
极度怀疑老师数据都从loj上面下载的。
建立一个超级源点 \(0\),连接每个点建电站的费用,跑一边 \(kurskal\) 最小生成树,叠加答案即可。
【Code】
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<set>
#include<algorithm>
#include<cmath>
#include<vector>
#define ll long long
#define ull unsigned long long
#define M 300000
#define N 13000
#define INF 0x3f3f3f3f
using namespace std;
/*-----------------------------------*/
int n,m,k,cnt;
struct node{
int u;
int v;
int dis;
}e[M];
int head[N];
int fat[N];
/*-----------------------------------*/
bool CMP(node a,node b)
{
return a.dis < b.dis;
}
int find(int x) {return fat[x] == x ? x : find(fat[x]);}
/*------------------------------------*/
int main()
{
/// freopen("irrigate.in","r",stdin);
// freopen("irrigate.out","w",stdout);
scanf("%d",&n);
for(int i = 1;i <= n;i ++) fat[i] = i;
for(int i = 1;i <= n;i ++) {
int x;
scanf("%d",&x);
e[++ cnt].u = 0;
e[cnt].v = i;
e[cnt].dis = x;
}
for(int i = 1;i <= n;i ++) {
for(int j = 1;j <= n;j ++) {
int mon;
scanf("%d",&mon);
if(i == j) continue;
e[++ cnt].u = i;
e[cnt].v = j;
e[cnt].dis = mon;
}
}
int tot = 0;
ll ans = 0;
sort(e + 1,e + cnt + 1,CMP);
for(int i = 1;i <= cnt;i ++) {
int x = e[i].u;
int y = e[i].v;
int s1 = find(fat[x]);
int s2 = find(fat[y]);
if(s1 != s2) {
fat[s1] = s2;
tot++;
ans += e[i].dis;
}
if(tot == n) break;
}
printf("%lld",ans);
return 0;
}
C 决斗
考试推了个假结论,没想到还得到了 \(10\) \(pts\) , 意料之外。
这题是一道博弈论的题目,以前没接触过,所以在考场上连题目意思都没搞懂。
考试时存边的时候和他差不多,但是我正序遍历,从入度为零的情况开始遍历,
先手后手交替,成功骗到了 \(10\) \(pts\) 。
考场上以为是先手后手轮换,没想到前面可以影响后面,导致了我出错。
考场上想搞一个 \(DFS\) 序,统计出度和入度,按照 \(topo\) 序,看到底是先手还是后手,如果两次遍历到的先手后手顺序不一样,就标记为 \(3\) ,后手为 \(2\), , 先手为 \(1\) 。
但当时样例没过就很迷惑。
博弈论结论
- 先手获胜的局面为先手必胜态;
- 如果当前局面有一个先手必败态的后继局面,那么当前局面为先手必胜态;
- 先手必败的局面为先手必败态;
- 如果当前局面的所有后继局面都是先手必胜态,那么当前局面为先手必败态。
解题思路
题解
终止局面即出度为 \(0\) 的点,初始局面即入度为 \(0\) 的点。
对于数的数据,直接 \(DFS\) 转移即可。
对于一般的 \(DAG\) 的数据,我们需要先求出拓扑序。
由于是从出边指向的点转移,所以应该按照出度求拓扑序,即每次加入出度为 \(0\) 的点。
按照这样进行拓扑序转移即可。
个人理解
一个局面可已转化为先手必胜态和先手必败态的时候,如果让其成为先手必胜态话用反向存图 \(+ topo + DFS\) 就可以得出他的状态。
【Code】【std思路】
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<set>
#include<algorithm>
#include<cmath>
#include<vector>
#define ll long long
#define ull unsigned long long
#define M 402000
#define N 130000
#define INF 0x3f3f3f3f
using namespace std;
/*-----------------------------------*/
int n,m,k,cnt,num;
struct node{
int next;
int to;
}e[M];
int head[N];
int fat[N];
int in[N];
int out[N];
int dis[N];//记录 0 1
struct nod{
int dian;
bool flag;
}a[N];
vector <int> E[M];
int f[N];
/*-----------------------------------*/
void add(int from,int to)
{
e[++ cnt].to = to;
e[cnt].next = head[from];
head[from] = cnt;
return;
}
//void dfs(int x,int fa)
//{
// if(dis[x]) return;
// if(fa == -1) dis[x] = 1;
// else if(dis[x] != 0 && (dis[fa] == dis[x])) dis[x] = 3;//标志
// else if(dis[x] == 0 && dis[fa] == 1) dis[x] = 2;
// else if(dis[x] == 0 && dis[fa] == 2) dis[x] = 1;// 1 为 对 2 为 错
// //cout << x << " " << fa << " " << dis[x] << endl ;
// for(int i = head[x];i;i = e[i].next) {
// int y = e[i].to;
// dfs(y,x);
// }
// return;
//}
/*------------------------------------*/
int main()
{
// freopen("duel.in","r",stdin);
// freopen("duel.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i ++) {
int x,y;
scanf("%d%d",&x,&y);
E[x].push_back(y);
add(y,x);
in[x] ++;
//out[y]++;
}
num = 0;
for(int ok,i = 1;i <= n;i ++) {
if(!in[i]){//终止局面
scanf("%d",f + i);
a[++ num].dian = i;
a[i].flag = true;
//dis[i] = ok;
}
}
for(int x = 1;x <= n;x ++) {
int s = a[x].dian;
for(int i = head[s];i;i = e[i].next) {
int y = e[i].to;
if(!--in[y]) a[++num].dian = y;
//cout << y << endl;
}
}
for(int x = 1;x <= n;x ++) {
int s = a[x].dian;
if(a[s].flag) continue;
//printf("%d\n",s);
f[s] = 0;
for(int i = 0;i < E[s].size();i ++) {
int y = E[s][i];
if(f[y] == 0) f[s] = 1;
}
}
for(int i = 1;i <= n;i ++) {
printf(f[i] ? "First\n" : "Last\n");
}
return 0;
}
【Code】
参照szt 大佬思路
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<set>
#include<algorithm>
#include<cmath>
#include<vector>
#define ll long long
#define ull unsigned long long
#define M 402000
#define N 130000
#define INF 0x3f3f3f3f
using namespace std;
/*-----------------------------------*/
int n,m,cnt;
struct node{
int next;
int to;
}e[M];
int head[N];
int in[N];
int ans1[M],ans2[M];
queue <int> qp;
/*-----------------------------------*/
void add(int from,int to)
{
e[++ cnt].to = to;
e[cnt].next = head[from];
head[from] = cnt;
return;
}
void topo()
{
for(int i = 1;i <= n;i ++) {
if(!in[i]) {
int x;
qp.push(i);
scanf("%d",&x);
if(x) ans1[i] = 1,ans2[i] = 0;//先手必赢态
else ans1[i] = 0,ans2[i] = 1;
} else ans1[i] = 0,ans2[i] = INF;//否则则赋值为
}
while(qp.size()) {
int s = qp.front();
qp.pop();
for(int i = head[s];i;i = e[i].next) {
int y = e[i].to;//因为是拓扑排序遍历,在此之前的ans都已经求出
ans1[y] = max(ans1[y],ans2[s]);//这一轮的先手在下一轮会成为后手,所以它尽可能的选择下一轮作为后手能赢的情况
ans2[y] = min(ans2[y],ans1[s]);//这一轮的后手在下一轮会成为先手,所以它的对手会尽可能给它留下作为先手能输的情况
if(!--in[y]) qp.push(y);
}
}
}
/*------------------------------------*/
int main()
{
// freopen("duel.in","r",stdin);
// freopen("duel.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i = 1;i <= m;i ++) {
int x,y;
scanf("%d%d",&x,&y);
add(y,x);
in[x] ++;
}
topo();
for(int i = 1;i <= n;i ++) {
printf(ans1[i] ? "First\n" : "Last\n");
}
return 0;
}
小结
- 审题还是第一位的,我第一题就没看懂题目。
- 看数据范围。
- 开 \(long \ \ long\)。
- 在不确定的情况下要进行测试点分治,保住一定能够拿到的分数。

浙公网安备 33010602011771号