2022/11/25 PTA AC 5/13
返乡,隔离,没能完整训练
一、N只老虎爱跳舞
题目描述
某动物园里有N(1≤N≤100)只爱跳舞的老虎,被编号为1~N,平时喜欢切磋舞技并且互相不服,于是动物园给他们安排了M(1≤M≤4000)场斗舞比赛,每场比赛在两只老虎之间进行。动物园试图根据比赛结果给出每只老虎的舞技排名(假设舞技高的老虎在每场比赛中都一定会胜过相对舞技差的老虎),但是由于比赛场数有限,并不能知道有些老虎的确切舞技排名。请你给出能确定舞技精确排名的老虎的数量。确保结果不会矛盾,但是同两只老虎之间可能重复比赛(结果相同)。
输入格式
输入第一行给出两个数字N(1≤N≤100)和M(1≤M≤4000)。
接下来M行,给出M场比赛的结果A和B,表示A老虎在这场比赛中击败了B老虎。
输出格式
输出一个数字,表示能确定舞技精确排名的老虎的数量。
tag
Floyd
思路
老虎之间的输赢关系存在传递性,借助多源Floyd最短路求得每两对老虎之间的输赢关系。最后答案统计,对于每个老虎如果它与其他其中的某个老虎确定不了输赢关系则答案+1
代码
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 110;
int dis[maxn][maxn];
int n, m;
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j) dis[i][j] = inf;
int x, y;
while (m--) {
scanf("%d%d", &x, &y);
dis[x][y] = 1;
}
for (int k = 1; k <= n; ++k) {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
}
}
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (j == i) continue;
if (dis[i][j] == inf && dis[j][i] == inf) {
ans++;
break;
}
}
}
printf("%d\n", n - ans);
return 0;
}
二、掩体计划
题目描述
掩体计划是《三体》中人类为了躲避高级文明打击的计划,其核心是在木星背阳面建立太空城,躲避光粒对太阳的打击。
为了不同的需求建立了不同的形状的太空城,例如有一个棍状的太空城,围绕其中心旋转,这样的设计可以使它的不同位置出现不同重力,便于不同作物的生长。
更有趣的是,太空城设计时引入了模块化的概念(软件工程,神!),每个太空城都有一些接口,可以跟其他太空城相连接。连接好的太空城可以具备更多功能,仿佛乐高一般。
在太空城建造完成之后,联邦主席想要测试太空城的耦合能力,他希望将所有的太空城相连成一个大太空城。不同的太空城间相连花费不同,主席为了预估工程的下界,希望你找到这样做的最小花费。
输入格式
输入的第一行是两个整数n,m(2≤n≤5000,1≤m≤10000),分别代表太空城的个数,和可以连接的太空城的关系数。
接下来m行每行有三个整数u,v,w(1≤u,v≤n,1≤w≤10^5,u≠v),代表u号太空城和v号太空城之间可以连接,其花费为w。
输出格式
假如太空城可以全部相连成一个大太空城,输出其最小花费。
否则输出Ridiculous! We can not combine city like a LOGO!!!
tag
最小生成树
思路
最小生成树模板题。注意题目中可能无法连接所有太空城,稀疏图使用 kruksal 算法求解只需要检查全图是否只有一个并查集
代码
#include<iostream>
#include<vector>
#include<algorithm>
#include<tuple>
using namespace std;
#define all(x) x.begin(), x.end()
const int N = 1e5 + 10;
int fa[N];
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) {
x = find(x), y = find(y);
if (x != y)
fa[x] = y;
}
void solve() {
int n, m;
cin >> n >> m;
vector<tuple<int, int, int>> road;
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
road.push_back(tuple<int, int, int>(u, v, w));
}
sort(all(road), [](tuple<int, int, int> a, tuple<int, int, int> b) {
return get<2>(a) < get<2>(b);
});
for (int i = 1; i <= n; ++i) fa[i] = i;
int ans = 0;
for (auto [u, v, w] : road) {
if (find(u) != find(v)) {
merge(u, v);
ans += w;
}
}
int flag = 0;
for (int i = 1; i <= n; ++i) {
if (fa[i] == i) ++flag;
if (flag == 2) {
cout << "Ridiculous! We can not combine city like a LOGO!!!" <<endl;
return;
}
}
cout << ans << endl;
}
int main() {
solve();
return 0;
}
三、你为什么不直接打死他?
题目描述
游戏炉石传说是一个双人对决的卡牌游戏,在这个游戏中,每个玩家控制一个英雄角色,并可以召唤随从为自己作战。
你无需了解这个游戏的具体规则,只需考虑这样一个问题。
精灵龙是一个具有2点生命值的随从。奥术飞弹是一张法术卡,打出这张卡可以随机等概率选择一个敌人(存活的精灵龙,以及一个敌方英雄),对其造成一点伤害,这个过程重复3次。随从受到伤害会相应的减少生命值,当一个随从生命值降到0时,它会立即死亡,之后就不会再被选为目标了(即使是同一张奥术飞弹释放的后续的飞弹也不会再选择它)。
在这个过程中,你无需考虑敌方英雄,所以你可以简单的看作打中敌方英雄相当于落空,也就是说假如场上有k只存活的精灵龙,每只精灵龙被打中的几率是
给出两个整数n和m,你需要计算出使用m张奥术飞弹,可以击杀掉n只精灵龙的概率。
输入格式
输入的第一行是两个整数n,m(1≤n≤10,1≤m≤10),含义如上所述。
输出格式
输出一行一个浮点数,对于你的答案res和标准答案ans,假如∣ans−res∣≤10^-6,我们就认为你的答案是正确的。
思路
观察题目中的奥术飞弹,是攻击三次,但是这三次的判定却都是独立的。所以,我们可以直接将奥术飞弹的数量m变成3m ,然后将每一次的奥数飞弹都当作是攻击一次。
下文中的奥术飞弹都将认为是只会攻击一次,并且m为输入的m的三倍。
这时,题目中的条件转化为有m张奥数飞弹。
我们考虑 概率dp 。
我们定义dp[i][j][k] 表示使用了i张奥数飞弹后,场上剩下j个2血的精灵龙和k个1血的精灵龙的概率。
考虑状态转移,对于一个已经确定的状态dp[i][j][k] ,使用一张奥术飞弹,可以分成三种情况。
打中敌方英雄。此时场上有j+k个精灵龙,打中地方英雄的概率为1.0/(j+k+1),状态转移为dp[i+1][j][k]+=dp[i][j][k]*1.0/(j+k+1)。
打中一个2血的精灵龙(需要保证此时场上有至少一个2血精灵龙)。此时场上有j+k个精灵龙,打中一个2血精灵龙的概率为1.0*k/(j+k+1) ,状态转移为dp[i+1][j-1][k+1]+=dp[i][j][k]*1.0*j/j(j+k+1)。(需要注意的是打中一个2血精灵龙之后他将变成一个1血精灵龙)
打中一个1血的精灵龙(需要保证此时场上有至少一个1血精灵龙)。此时场上有j+k个精灵龙,打中一个1血精灵龙的概率为1.0*k/(j+k+1) ,状态转移为dp[i+1][j][k-1]+=dp[i][j][k]*1.0*k/(j+k+1)。(打中一个1血精灵龙之后这个精灵龙将会死亡)
考虑初始状态,初始的时候,使用0张奥术飞弹,场上有n个2血精灵龙和0个1血精灵龙,这种情况的概率为1,即dp[0][n][0]=1.0。
考虑终止状态,最终要求的是使用完奥数飞弹之后,场上没有精灵龙的概率,即dp[m][0][0] 。
之后就是对这些状态进行遍历和转移即可。
代码
#include<bits/stdc++.h>
#define Inf 0x3f3f3f3f
using namespace std;
typedef long long LL;
typedef pair<int, int> P;
const int MAXX = 50;
int n, m;
double dp[3 * MAXX][MAXX][MAXX];
inline void solve() {
scanf("%d%d", &n, &m);
m *= 3;
dp[0][n][0] = 1.0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j <= n; ++j)
for (int k = 0; k <= (n - j); ++k) {
dp[i + 1][j][k] += dp[i][j][k] * 1.0 / (j + k + 1);
if (k >= 1)
dp[i + 1][j][k - 1] += dp[i][j][k] * 1.0 * k / (j + k +1);
if (j >= 1)
dp[i + 1][j - 1][k + 1] += dp[i][j][k] * 1.0 * j / (j +k + 1);
}
}
printf("%.8lf\n", dp[m][0][0]);
}
signed main() {
solve();
return 0;
}
四、高维世界
题目描述
在《三体》中,人类舰队中的万有引力号和蓝色空间号进入到了四维世界。在三维世界中,一个坐标可以视作(x,y,z)的三元组,进入四维世界后,则需要四元组(x,y,z,w)才能唯一定位了。
在三维世界中建造出的导航系统因为缺乏高维感知能力,无法继续使用,舰长找到了你希望你能开发一个程序,辅助飞船运行。因为不知道四维空间内是否可能通往更高维的空间,舰长需要你开发一个适配各个维度的程序。为了验证你的程序是否正确,舰长会给你n个k维空间的点,你需要回报这些点中,最近的点对的距离是多少。
在这里定义两个k维点(x1,x2,...,xk)和(y1,y2,...,yk)的距离
输入格式
输入的第一行是两个整数n,k(2≤n≤5000,1≤k≤10),分别代表点的个数和维度。
接下来n行包含k个整数,每个数的绝对值小于10^5,按顺序标识一个k维点的坐标。
输出格式
为了避免误差,假设所有点对的最小距离为ans,请你输出ans^2的值。
题目数据保证答案在64位整数范围内
tag
暴力/最近点对
思路
最近点对分治法可以很高效,但实现起来非常复杂。仔细看本题数据点的个数不超过5000,二重循环暴力查找即可
k维空间点坐标可以通过结构体或多维数组存储
注意数据范围会爆int改为使用long long
代码
#include<iostream>
using namespace std;
typedef long long ll;
struct point {
ll x[15]; //记录不同维度的坐标
} p[5005];
void solve() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
cin >> p[i].x[j];
}
}
ll ans = 1e18;
auto dis = [&](point a, point b) {
ll res = 0;
for (int i = 1; i <= k; ++i) {
res += (a.x[i] - b.x[i]) * (a.x[i] - b.x[i]);
if (res >= ans) return ans; //剪枝
}
return res;
};
for (int i = 1; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
ll temp = dis(p[i], p[j]);
if (temp < ans) ans = temp;
}
}
cout << ans << endl;
}
int main() {
solve();
return 0;
}
五、不废话了
题目描述
给定n个数的数组A,标号从1到n,定义好的三元组为一个三元组(i,j,k)为同时满足以下条件的三元组:
1≤i<j<k≤n
其中&是按位与运算,⌊x⌋代表将x向下取整,如⌊4.3⌋=4。
有m次操作,每次操作给出x,y,代表将数组第x个元素Ax置为y。你需要在每次操作后,输出数组中好的三元组的个数。
请注意,操作是继承的,也就是进行第i+1次操作时,前i次操作的影响依旧存在。
输入格式
输入的第一行为两个整数n,m,3≤n≤10^5,1≤m≤10^5,代表数组长度和操作数。
接下来一行包含n个整数,代表数组元素。
接下来m行包含两个整数x,y,1≤x≤n,代表操作。
保证数组的元素始终是正整数且绝对值小于等于10^9。
输入格式
输出m行整数,每一个整数代表对应操作后满足条件的三元组数。
思路
枚举或者推式子可以发现,好的三元组的元素i,j,k满足i/2=j/2=k/2,因此可以考虑用桶数组,记录每个数出现的次数,答案即为每个桶索引记录次数z的
组合数求和
每次操作继承因此在计算完答案后需要对应做出修改,即答案减去当前被修改索引记录次数贡献的答案,加上修改后新索引次数贡献的答案
注意数据范围会爆int改为使用long long
代码
#include<iostream>
#include<map>
#include<cstring>
using namespace std;
#define maxn 100010
int cal(int x) {
if (x < 3) return 0;
return (x - 1) * x * (x - 2) / 6;
}
int getid(int x) {
return x / 2;
}
int yuan[maxn];
void solve() {
memset(yuan, 0, sizeof(yuan));
map<int, int> cnt;
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) {
int t;
cin >> t;
t = getid(t);
yuan[i] = t;
cnt[t]++;
}
int ans = 0;
for (auto [k, v] : cnt) {
ans += cal(v);
}
while (m--) {
int x, y;
cin >> x >> y;
y = getid(y);
ans -= cal(cnt[yuan[x]]);
ans -= cal(cnt[y]);
cnt[yuan[x]]--;
cnt[y]++;
ans += cal(cnt[y]);
ans += cal(cnt[yuan[x]]);
yuan[x] = y;
cout << ans << endl;
}
}
int main() {
solve();
return 0;
}
六、阶梯计划
题目描述
在《三体》的故事中,人类遭受三体星人的入侵,三体舰队正在前往地球远征的路上,人类需要找到方法反制。
阶梯计划,是由行星防御理事会战略情报局(PIA)所提出的构想。主要内容是通过核爆将飞行器加速,将探测装置发送到三体舰队,以获取三体星人的情报。计划不断迭代,PIA最终决定将一个地球人送往三体世界做间谍(三体人对人类也有浓厚的兴趣,所以也乐于接受这个间谍)。
三体人科技发达,可以通过细胞技术还原完整的人类。因此PIA可以不发送整个人体,而是发送部分组织。
除了发送的人体之外,飞船上还需要装载一些必要的设备用于星际航行,比如维持生命的必要设备等。
由于技术限制,飞船的容量有限,因此你需要对发送那些组织做出抉择,不同的组织有不同的重量和价值,PIA希望在不超过容量的情况下选择价值最大的方案。
具体地说,我们给出飞船的容量W,以及若干个物体的集合O,和一个必需物体集合P∈O。每个物体标识为二元组{w,v}。其中w为其体积,v为其价值。我们定义一个集合S可行,意为S满足,定义一个集合S的收益为
,请你找到所有可行集合中最大的收益。
输入格式
输入数据的第一行是一个整数W,1≤W≤10^9,含义如前所述。
输入数据的第二行是一个整数n,1≤n≤10^3,代表物体的数量
接下来的n行中,第i行包含空格分割的两个整数wi,vi,代表第i个物品的属性,如果vi=−1,代表第i个物品是必须物品。
保证
输出格式
假如飞船无法塞满必需品或者在塞满必需品的情况下不能塞入任何一个组织,请打印failed!来宣告阶梯计划的终止。
否则,打印一行一个整数,代表最大收益。
tag
01背包变式
思路
在存储过程中用 先减去必需品占用的容量,并找出所有组织中最小占用容量和最大价值
根据题意如果剩余的容量 为负数或者装不下任何一个组织,即小于上一步骤中最小占用容量,则任务失败
反之则肯定存在答案,不难发现问题转化为了类似01背包问题的在组织中寻找最大收益
如果套用课上学习的以容量维度进行动态规划,占用空间太大可以考虑转为用收益作为维度,思路不变,具体参考博客
代码
#include<iostream>
#include<cmath>
#include<vector>
using namespace std;
void solve() {
int W;
cin >> W;
int n;
cin >> n;
vector<int> other_w, other_v;
int minw = 1e9, maxv = -1;
for (int i = 1; i <= n; ++i) {
int w, v;
cin >> w >> v;
if (v == -1)
W -= w;
else {
other_w.push_back(w), other_v.push_back(v);
minw = min(minw, w);
maxv = max(maxv, v);
}
}
if (W < 0) {
cout << "failed!" << endl;
return;
}
n = other_w.size();
int ans = -1;
vector<int> dp(n * maxv + 1);
fill(dp.begin(), dp.end(), 1e9);
dp[0] = 0;
for (int i = 0; i < n; ++i) {
for (int j = n * maxv; j >= other_v[i]; --j) {
dp[j] = min(dp[j], dp[j - other_v[i]] + other_w[i]);
if (dp[j] <= W && j > ans)
ans = j;
}
}
cout << ans << endl;
}
int main() {
solve();
return 0;
}
七、鱼饵桶
题目描述
游戏炉石传说是一个双人对决的卡牌游戏,在这个游戏中,每个玩家控制一个英雄角色,并可以召唤随从为自己作战。
你无需了解这个游戏的具体规则,只需考虑这样一个问题。
随从卡是可以召唤到场上的卡,它有两个属性,攻击力和生命值,我们可以用一个二元组(a,h)来标识它。
鱼饵桶是一张法术卡,它可以让所有手牌中(即未使用的)鱼人随从卡的攻击力和生命值都加1,场上(即已使用的)每有一张鱼人随从,它会额外再重复一次。
举个例子,假如你有三张鱼人随从,属性分别为(1,1),(2,1),(3,2),标号为1,2,3,先释放1号随从到场上,之后释放一张鱼饵桶,那么这张鱼饵桶会释放1+1=2次,在手牌中的2号和3号随从的属性值会相应变为(4,3)和(5,4)。
现在给出n张随从和m张鱼饵桶,你能不受限制地以任何顺序打出随从或者鱼饵桶,除了释放次数外,释放它们没有任何额外限制。现在你需要回答这样几个问题:
1.在所有可能的释放顺序中,所有随从最终的攻击力总和最大为多少?(即,最大化
)
2.在所有可能的释放顺序中,所有随从最终的生命值总和最大为多少?(即,最大化
)
3.在所有可能的释放顺序中,所有随从中最终的攻击力最大为多少?(即,最大化
)
4.在所有可能的释放顺序中,所有随从中最终的生命值最大为多少?(即,最大化
)
输入格式
输入的第一行是两个整数n,m(1≤n≤10^4,1≤m≤10^5),含义如上所述。
接下来的n行每行包括两个整数a,h,(1≤a,h≤10^9)。代表随从的属性。
输出格式
输出一行四个空格分割的整数ans1,ans2,ans3,ans4,代表上述四个问题的答案。
tag
贪心/构造
思路
对于前两个问题:
演算几次或者推导一下可以发现最优解是:先将一部分随从卡牌x(0≤x≤n)上场,然后将m张鱼饵桶使用掉,最后再将剩余的所有随从卡牌上场。那么所有随从卡牌提升的属性值之和可以写成如下函数:
f=m(x+1)(n-x)
其中m恒为正整数,f(x)为开口向下的一元二次函数,最优解在(n-1)/2。
![](https://i-blog.csdnimg.cn/blog_migrate/6190b340869fc0d5e9463f76e8680665.png)
对于后两个问题:
很容易贪心想到将属性值最大的随从卡牌进一步最大化即可。因此挑出属性值最大的随从卡牌,先将其他n-1张随从卡牌上场,然后将m张鱼饵桶使用掉,最终再将保留的那张随从卡牌上场
代码
#include<iostream>
#include<vector>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pii;
void solve() {
ll n, m;
cin >> n >> m;
ll suma = 0, sumh = 0, maxa = 0, maxh = 0;
for (ll i = 1; i <= n; ++i) {
ll a, h;
cin >> a >> h;
suma += a, sumh += h;
maxa = max(maxa, a), maxh = max(maxh, h);
}
ll l = (n - 1) / 2;
cout << suma + m * (l + 1) * (n - l) << ' ';
cout << sumh + m * (l + 1) * (n - l) << ' ';
cout << m + (n - 1) * m + maxa << ' ';
cout << m + (n - 1) * m + maxh << endl;
}
int main() {
solve();
return 0;
}
八、环球旅行
题目描述
寒假将至,小鹏同学决定规划一次环球旅行。已知他有n个想去的国家,编号为1−n,并且存在m条道路连接这些国家。可贫困的小鹏同学资金有限,他决定从这n个想去的国家中选择一些国家作为本次假期的旅行景点,其他国家不再考虑前往。
小鹏同学可以自由规划这些国家先后访问的顺序,但他不喜欢在旅行过程中访问曾经去过的国家,并且他的父母可以开车送他前往起点国家,待他旅行结束后再在起点国家接他回家,但旅行期间其余国家之间的穿梭以及旅行结束后回到起点国家需要小鹏同学自己付车费。小鹏同学听说你在学算法课,所以想让你帮他写个程序算一下本次旅行需要支付的最少车费。
输入格式
输入第一行有两个正整数n(1≤n≤10)和m(1≤m≤200)分别代表此次小鹏同学想去的国家数量以及这些国家之间的道路数量。
接下来m行,每一行代表两座国家之间有一条道路,给出三个正整数u,v,w(1≤u,v,w≤10)分别代表两座国家和它们之间道路的车费,注意两座国家间可能有多条道路。
紧接着一行有一个正整数q(1≤q≤500)代表小鹏同学当前备选的计划数量。
接下来q行,每一行代表一个备选计划,给出的第一个正整数t(2≤t≤10)表示该计划选择的国家数量,后面t个空格间隔的正整数c1,c2,...,cn(1≤ci≤n)代表这t个计划选择的国家编号。
输出格式
输出共q行,第i行输出小鹏同学第i个备选计划最少的花费,如果计划旅行的国家根本无法全部访问则输出−1。
tag
TSP
思路
每次询问其实就是让判断给定点集是否存在tsp路径,已知tsp问题算法复杂度为,如果从每个起点开始均跑tsp求解耗费时间过长,但其实不难发现在此问题的无向图中tsp问题本质与起点无关。
进一步分析此题其实跑一遍tsp问题对于每个询问寻找答案单独处理可以进一步加快速度
代码
#include <bits/stdc++.h>
using namespace std;
int n, m;
const int maxn = 15;
int dp[maxn][1 << 15][maxn];
vector<pair<int, int>> e[maxn];
int dis[maxn][maxn];
int main() {
cin >> n >> m;
memset(dis, 0x3f, sizeof(dis));
while (m--) {
int u, v, w;
cin >> u >> v >> w;
e[u].push_back({ v, w });
e[v].push_back({ u, w });
dis[u][v] = dis[v][u] = min(dis[u][v], w);
}
int q;
cin >> q;
memset(dp, 0x3f, sizeof dp);
for (int s = 1; s <= n; s++) {
dp[s][1 << (s - 1)][s] = 0;
for (int sta = 0; sta < (1 << n); sta++) {
for (int u = 1; u <= n; u++) {
if (dp[s][sta][u] == 0x3f3f3f3f)
continue;
for (auto [v, w] : e[u]) {
if (sta & (1 << (v - 1)))
continue;
dp[s][sta | (1 << (v - 1))][v] = min(
dp[s][sta | (1 << (v - 1))][v],
dp[s][sta][u] + w);
}
}
}
}
while (q--) {
int sz;
cin >> sz;
vector<int> tmp;
while (sz--) {
int x;
cin >> x;
tmp.push_back(x);
}
int sta = 0;
for (auto i : tmp) {
sta |= (1 << (i - 1));
}
int ans = 0x3f3f3f3f;
for (auto u : tmp)
ans = min(ans, dp[tmp[0]][sta][u] + dis[u][tmp[0]]);
if (ans == 0x3f3f3f3f)
cout << -1 << endl;
else
cout << ans << endl;
}
}