A - Minimum’s Revenge_规律
题意:
给你n个点,每个点编号从1-n,任意两个点之间连线的权值为这两个点编号的最小公倍数,问这n个点的最小生成树的权值和。
题解:
设任意两个点编号的最小公倍数肯定大于等于最大的编号,编号1与其他编号的最小公倍数等于其他编号,所以直接把其他编号的点与编号为1的点相连就是最小生成树了,权值和为 2 + 3 + ······ + n,所以公式:
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int main()
{
int T; scanf("%d", &T);
int len = 0;
while(T--)
{
ll n; cin>>n;
printf("Case #%d: %lld\n", ++len, (2 + n) * (n - 1) / 2);
}
}
B - Prediction_并查集+思维+dfs
题意:
给你一个一棵有n个点m条边的图,再给你一个与这个图对应的m个节点的树,树上的每个节点对应图中的一条边。
然后进行q次查询,每次查询给你一个树中点的集合,对于每次查询应计算:这个集合中的所有点 and 集合中点的所有祖先节点所代表的边,把这些边连起来所构成的联通块的个数。
题解:
首先合并问题考虑到并查集,然后由于查询的时候每个点以及这个点对应其所有祖先节点所代表的边都要连起来,那么如果每次都遍历集合中的点以及其所有祖先节点显然不合理,很可能超时。 所以可以让每一个节点都维护一个并查集(该节点的边以及其所有祖先节点对应的边都并在该节点维护的并查集中了)。
由于题目的特殊性,查询时给你的集合中的点,其父亲节点也算,所以可以从父亲节点往下推。根据这个特点,建立每个点所维护的并查集时,不应该每次从头遍历,太费时间了,可以继承其父亲的并查集然后再加并上自身的边。这样一来dfs一次就把所有节点的对应的并查集都初始化好了。
以后查询的时候只需要把对应集合中的每个点维护的并查集都再并一下,记录合并的边数,最后用总边数减去其就是答案了。
如果不明白可以直接看明白题目后看代码,有详细注释。
代码:
#include<iostream>
#include<cstdio>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#define up(i, x, y) for(int i = x; i <= y; i++)
#include<cmath>
#define MAXN ((int)1e4 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
// 说明:树上的点k作为注释里面所说的维护的k号并查集,也就是树上节点是多少维护的并查集就是几号
int n, m;
vector<int> G[MAXN]; //存树
pair<int,int> e[MAXN]; //存图的边
int pre[MAXN][500 + 10]; //每个顶点维护一个并查集
int d[500 + 10];
inline int find_(int x, int t) // 查询t点维护的t号并查集
{
return x == pre[t][x] ? x : pre[t][x] = find_(pre[t][x],t);
}
//加上inline快了将近600ms,所以向这种常用的,且简短递归程度不高的语句最好写成内联函数
//定义为内联函数,再编译的过程中就会把这个函数放到需要它的地方,不用调用了,但是会增加内存大小
void dfs(int u, int fa)
{
up(i, 1, n) pre[u][i] = pre[fa][i]; // 接用父亲节点已经处理好的并查集
int fx = find_(e[u].first, u);
int fy = find_(e[u].second, u);
if(fx != fy) pre[u][fx] = fy;
//合并边,树上的u这个点对应着的边是从点e[u].first到e[u].second
//所以不仅要继承父亲并查集的内容,还要顾到自己的边
for(int i = 0; i < G[u].size(); i++)
{
int v = G[u][i];
dfs(v, u); //往下”继承“,也就是接着维护u子节点对应的并查集。
}
}
int main()
{
int T; scanf("%d", &T);
up(cas, 1, T)
{
scanf("%d %d", &n, &m);
up(i, 1, m) G[i].clear(); //初始化
up(i, 2, m)
{
int t; scanf("%d", &t);
G[t].push_back(i); // t 连向 i , 父节点指向子节点,建树
}
up(i, 1, n) pre[1][i] = i;//对1号并查集进行初始化,也就是父节点对应的并查集
up(i, 1, m) scanf("%d %d", &e[i].first, &e[i].second);
//树上每个顶点对应着的边
dfs(1, 1);
int q; scanf("%d", &q);
printf("Case #%d:\n", cas);
while(q--) //每一个查询,用0号并查集把他们并起来
{
int t; scanf("%d", &t);
up(i, 1, n)
{
pre[0][i] = i; // 0号并查集初始化
}
int ans = n; //初始有n块联通
up(i, 1, t)
{
int a; scanf("%d", &a);
up(j, 1, n) //合并a号并查集
{
int fy = find_(j, a);//查询a号并查集
if(fy != j)
// 不等于j说明j这个点不是孤立的,而是a号并查集所并起来的点,所以要把这个点用0号并查集合并起来。
{
int x = find_(fy, 0); //在0号并查集中查询
int y = find_(j, 0);
if(x != y)
{
ans--; //合并了,所以少一个联通
pre[0][x] = y; //更新0号并查集
}
}
}
}
printf("%d\n", ans);
}
}
return 0;
}
是否用内联函数的效果对比:
使用内联函数 :时间:1014 ms 内存:23.6 MB
未使用内联函数:时间:1606 ms 内存:22.8 MB
C - Mr. Frog’s Problem_数学
题意:
略
题解:
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int main()
{
int T; cin>>T;
int len = 0;
while(T--){
ll a, b; cin>>a>>b;
if(a == b){
printf("Case #%d:\n1\n%lld %lld\n", ++len, a, b);
}
else{
printf("Case #%d:\n2\n%lld %lld\n%lld %lld\n", ++len, a, b, b, a);
}
}
}
D - Coconuts_坐标离散化技巧
题意:
给你一个R * C的矩阵,然后给你n个点,问你由这n个点把这个矩阵分成了几大块,从小到大的顺序输出每一大块里1*1的小块的数目。0<R,C≤10^9 0≤n≤200
题解:
首先观察数据范围,这个矩阵最大为 10^9 * 10^9 的一个矩阵,太大了,bfs直接搜根本不可行,再来观察n,它最大才200,所以可以把这个矩阵离散化(乘坐标离散化,具体算法举例可参照小白皮(挑战程序设计竞赛) P164)
利用坐标离散化把矩阵主要特征(这n个点的整体相对位置)保留,还有一个麻烦的地方就是让求每大块内的个数,如果离散化后个数的特征就会消失。所以可以设一个数组P,来保存这个特征,也就是在压缩矩阵的过程中计算压缩后的一小块是由之前的多少块压缩过来的。
代码会有详细注释,还有不明白的可参照代码注释。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int T, n, m, sum, len, e;
int nxt[4][2] = {0, -1, -1, 0, 0, 1, 1, 0}; // bfs遍历用的
ll P[1000][1000]; //保留每一小块是由几个小块压缩过来的
int x1[1000], y1[1000]; //保留那n个点
bool path[1000][1000]; //记录离散化之后的矩阵
ll ans[1000]; //保留结果,最后升序输出
int tt[2]; //离散化之后返回压缩后的矩阵的高和宽
bool vis[1000][1000]; //bfs标记用
int loc;
void compress(int *x1, int *x2, int n, int m)
{
int tmp[2];
vector<int> xs, ys;
xs.push_back(0); xs.push_back(n);//为了计算每一小块是由多少块压缩过来的,需要把端点加入进去,保留足够特征
ys.push_back(0); ys.push_back(m);//比如(0, 0)点之后的点想计算压缩之前的块数,需要往前减和往上,可以把增
//加的两端的点理解为两边放了两堵墙,卡住,为了可以计算压缩后每个点代表的实际块数
for(int i = 1; i <= len; i++){
for(int d = -1; d <= 1; d++){ //每一关键点的相邻的地方保留相关信息
int dx = x1[i] + d, dy = y1[i] + d;
if(dx >= 1 && dx <= n) xs.push_back(dx);// 关键信息点,为离散化做准备
if(dy >= 1 && dy <= m) ys.push_back(dy);
}
}
sort(xs.begin(), xs.end()); //把关键信息排序
sort(ys.begin(), ys.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end()); //删除重复的关键信息
ys.erase(unique(ys.begin(), ys.end()), ys.end());
for(int i = 1; i < xs.size(); i++){
for(int j = 1; j < ys.size(); j++){
P[i][j] = ll(xs[i] - xs[i - 1]) * ll(ys[j] - ys[j - 1]); //保留每一个点是由多少个点压缩过来的特征
}
}
for(int i = 1; i <= len; i++){
x1[i] = find(xs.begin(), xs.end(), x1[i]) - xs.begin(); // 求离散化之后那n个点的位置
y1[i] = find(ys.begin(), ys.end(), y1[i]) - ys.begin();
path[ x1[i] ][ y1[i] ] = 1; //保留那n个点的位置
}
tt[0] = xs.size() - 1; // 减去多加的 0这个点,当初加0也是为了计算每一个点是由多少个点压缩过来的特征,0不在矩阵内
tt[1] = ys.size() - 1;
}
void dfs(int x, int y)
{
path[x][y] = 1; //当vis用了,这样就少开一个vis数组
ans[loc] += P[x][y];//计算块数
for(int i = 0;i < 4; i++){
int dx = x + nxt[i][0], dy = y + nxt[i][1];
if(dx >= 1 && dx <= tt[0] && dy >= 1 && dy <= tt[1] && !path[dx][dy])
dfs(dx, dy);
}
}
int main()
{
scanf("%d", &T);
while(T--){
loc = 0;
memset(path, 0, sizeof(path));
memset(ans, 0, sizeof(ans));
scanf("%d %d", &n, &m);
scanf("%d", &len);
for(int i = 1; i <= len; i++){
scanf("%d %d", &x1[i], &y1[i]);
}
compress(x1, y1, n, m); //坐标离散化
for(int i = 1; i <= tt[0]; i++){ //bfs离散化之后的图,并记录答案ans[]
for(int j = 1; j <= tt[1]; j++){
if(!path[i][j]){
loc++;
dfs(i, j);
}
}
}
sort(ans + 1, ans + 1 + loc); //升序
printf("Case #%d:\n%d\n", ++e, loc);
up(i, 1, loc) printf("%lld%c", ans[i], i == loc ? '\n' : ' ');
}
}
E - Mr. Frog’s Game_暴力
题意:
给你一个连连看的图,每一种数字代表一种图标,问你是否可以消去一对,是输出Yes 反之
题解:
遍历第一行最后一行,第一列最后一列,如果有相同的那么可以连
遍历每一行每一列,看看是都有相邻的,如果有相邻的也可以连
否则不可以连,因为最初的图是满的那么连相邻的,要么连最外围的。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int G[50][50];
int main()
{
int T; scanf("%d", &T);
int len = 0;
while(T--){
int n, m ; scanf("%d %d", &n, &m);
up(i, 1, n) up(j, 1 ,m) scanf("%d", &G[i][j]);
map<int, int> mp;
mp.clear();
up(i, 1, m) // 第一行
{
mp[G[1][i]]++;
if(mp[G[1][i]] > 1){
goto stop;
}
}
mp.clear();
up(i, 1, m) //最后一行
{
mp[G[n][i]]++;
if(mp[G[n][i]] > 1){
goto stop;
}
}
mp.clear();
up(i, 1, n) //第一列
{
mp[G[i][1]]++;
if(mp[G[i][1]] > 1){
goto stop;
}
}
mp.clear();
up(i, 1, n) //最后一列
{
mp[G[i][m]]++;
if(mp[G[i][m]] > 1){
goto stop;
}
}
up(i, 1, n) //每一行
{
up(j, 2, m)
{
if(G[i][j] == G[i][j - 1]) goto stop;
}
}
up(i, 1, m) //每一列
{
up(j, 2, n)
{
if(G[j][i] == G[j - 1][i]) goto stop;
}
}
printf("Case #%d: No\n", ++len);
continue;
stop : {}
printf("Case #%d: Yes\n", ++len);
}
}
F - Auxiliary Set_思维+dfs
题意:
给你一棵树,然后又q次询问,每次询问会给你m个不重要的点,且规定两个重要的点的最小公共祖先也是重要的点,即使它在给定的不重要的点中。每次查询输出重要点的数目 = 开始给的重要的点的数目 + m个不重要的点中重要的点(这个重要的点就是两个重要点的最小公共祖先)。
题解:
这里不用tarjan算法去做,而把这个题作为一个思维+dfs的题目来看。(LCA 最近公共祖先,Tarjan(离线)算法感兴趣的推荐看这个博客:https://www.cnblogs.com/JVxie/p/4854719.html
假设一个不重要的点如果他有至少两个子树,并且至少两棵子树中每棵子树如果有重要的点,那么这个节点也就是重要的点。所以只需要用dfs遍历下求一下每个节点的直系儿子节点,也就是有几棵子树,然后记录下来。最后根据题目提供的不重要的点,先把这些不重要的点按照深度排序,然后就可以保证从树底部往树根推,如果一个点不是重要节点那么就删除,因为不是重要节点没什么用,只有从树底往上推才可以保证如果一个节点有两个以上的子树,那么该节点就是重要节点。
具体可以看代码,注释很详细。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
typedef long long ll;
int n, q;
vector<int> vec[MAXN];
int root = 1;
void init() {for(int i = 0; i <= n; i++) vec[i].clear(); }
int fa[MAXN], dep[MAXN], son[MAXN];
int tmp[MAXN], imp[MAXN];
void dfs(int now, int pre, int d) //pre是now的父节点
//now为当前节点 pre是当前节点的父节点 d是当前树的深度
{
fa[now] = pre; dep[now] = d; son[now] = 0;
for(int i = 0; i < vec[now].size(); i++) //找当前节点的子节点
{
int v = vec[now][i];
if(v == pre) continue;
// 如果当前节点的儿子等于当前节点的父亲,瞎扯,肯定不行,返回,这是由于存的双向边导致的这种情况发生
dfs(v, now, d + 1);//往下递归找下去
son[now]++;//递归回来的时候记录直系子节点的数目
}
}
bool cmp(int a, int b)
{
return dep[a] > dep[b]; //深度降序排序
}
int main()
{
int T; scanf("%d", &T);
for(int case_ = 1; case_ <= T; case_++)
{
printf("Case #%d:\n", case_);
init(); //初始化vector;
scanf("%d %d", &n, &q); // 树上n个点,q次询问
for(int i = 1; i < n; i++)
{
int a, b; scanf("%d %d", &a, &b);
vec[a].push_back(b);
vec[b].push_back(a);//建立双向边
}
dfs(1, 0, 0);//树根为 1
while(q--)
{
int x; scanf("%d", &x);//x是不重要节点的数目
for(int i = 1; i <= x; i++)
{
scanf("%d", &imp[i]); //不重要的点
tmp[ imp[i] ] = son[ imp[i] ];
//得到这个不重要的点有几个直系节点,也就是有几棵子树
//用tmp保留一下,防止后面删除操作时破坏了原来的son数组
}
sort(imp + 1, imp + 1 + x, cmp);
//把这些不重要的点按深度,从大到小排,也就是离根节点远的不重要的点在前面
//这是为了一会查询子树的时候,保证每个子树都可以提供一个有用的点
//因为是从树底部往上找的,如果一个节点是不重要的,那么就要把这个
//不重要的节点的父节点的子树减去1,这样一来从下往上走一遍就好了
int ans = 0;
for(int i = 1; i <= x; i++)
{
if(tmp[ imp[i] ] >= 2) ans++;
//如果子树大于等于两个,又由于从下至上找的,可以保证每个子树都有一个重要节点
//所以答案++
else if(tmp[ imp[i] ] == 0) tmp[ fa[ imp[ i ]]]--;
//如果为0的话,说明其父节点的该子树的无法提供重要节点,那么需要删去
}
printf("%d\n", ans + (n - x) ); // 新增的重要节点 + 原来题目提供的重要节点数目
}
}
}
H - Basic Data Structure_模拟+规律+双端队列
题意:
让你模拟实现一个新的类似于栈的数据结构,该数据结构可以具有以下操作:
1、插入到栈顶
2、从栈顶弹出
3、栈反转,栈头变栈底,栈底变栈头
4、查询,查询时从栈顶元素一直进行nand运算直到栈底,最后输出结果
0 nand 1 == 1 nand 0 == 0 nand 0 == 1 1 nand 1 == 0
题解:
对于栈可以用数组实现,最初位置在中间,规定一个方向dir,dir为正时就右边就是栈顶,反之左边是栈顶,这样就可以很容易实现反转还有插入删除操作。
对于查询操作,如果遍历一遍的话会超时,所以观察 nand 运算的特点,假设从右向左进行nand运算,那么当运算到最左边的0的时候一定是1,如果最左边的0右边还有其他元素。所以可以分四种情况讨论。
1、当没有元素的时候直接输出 Valid.
2、当栈中全部是1的时候,直接看1个数的奇偶性,如果是奇数最后是1,反之是0
3、设栈里有a个元素,拿当最右边是栈顶时举例子,这时候,如果最左边的0恰好是栈顶元素,说明这个栈中只有最右边的元素是0,这时候就可以看作有 a - 1 个1,然后根据1的个数的奇偶性就好了。
4、除了以上特殊的情况外,还是拿最右边是栈顶时举例子,这时候进行查询时,只需要关注最左边的0的位置就好,因为从右往左计算到最后一个0时,这时候一定是1,最后结果是多少就要看1的个数的奇偶性了。
为什么会有3、4这俩分开呢,举个例子:(设右边是栈顶)
① 如:1 1 0 最后计算到最左边的0,剩的1的位置应该是: 1 1
② 如:1 0 1 最后计算到最左边的0,剩的1的位置应该是: 1 1
虽然最后结果1的位置一样 ,但是你会发现,①中的运算到最左边的0的位置是0, 而②中的运算到最左边的0的位置是1,这会导致最后0的位置与最终1的个数有个1的差别,这里没明白没关系,看下代码就懂了,有详细注释。
可以用双端队列保存0的位置。
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)2e5 + 10)
#define INF 0x3f3f3f3f
using namespace std;
int st[MAXN * 2 ]; //开两倍的数组,因为会有反转操作,所以栈底为数组的中间位置
int l, r; //记录栈的两端位置
int dir; //记录是否反转,dir > 0, l是栈底,反之。
deque<int> que; // 双端队列,保存0的位置
typedef long long ll;
int main()
{
int T, pp = 0; scanf("%d", &T);
while(T--){
printf("Case #%d:\n", ++pp);
que.clear(); // 清零
l = MAXN, r = MAXN - 1; //栈初始化,当l > r栈为空
dir = 1; //栈初始化
int n; scanf("%d", &n);
while(n--){
char str[10]; scanf("%s", str);
if(str[2] == 'S'){ // 当PUSH操作时
int t; scanf("%d", &t);
if(dir) st[++r] = t; // 右边是栈顶,所以++r
else st[--l] = t; //反之
if(t == 0){ //用队列记录0所处位置
if(dir) que.push_back(r);
else que.push_front(l);
}
}
else if(str[2] == 'P'){
if(dir){
if(st[r] == 0) que.pop_back();
//如果删除的是0,那么队列里对应的0的位置也需要删除
r--;
}
else{
if(st[l] == 0) que.pop_front();//同上
l++;
}
}
else if(str[2] == 'E'){
if(r < l){puts("Invalid."); continue;} //栈空
if(que.empty()) {printf("%d\n", (r - l + 1) & 1); continue;}
//没有0,全是1,所以直接看1的个数是奇数还是偶数
// n是偶数 :n & 1 == 0; n是奇数: n & 1 == 1
if(dir){
if(que.front() == r) printf("%d\n", (r - l) & 1);
//如果右边是栈顶,那么看最左边0的位置,如果最左边的0在栈顶,那么就是有(r - l)个1
else printf("%d\n", (que.front() - l + 1) & 1);
//如果不是上面这种特殊情况,那么直接算最左边有几个相连的1就好,最左边的0也是1,
//因为最左边的0的右边还有数,只要有数,那么从栈顶到栈底的时候就会在这个0处产生1
}
else{
if(que.back() == l) printf("%d\n", (r - l) & 1);//同上
else printf("%d\n", ( r - que.back() + 1) & 1);
}
}
else{
dir = 1 - dir; // 1 - 0 == 1 ; 1 - 1 == 0; 反转 也可以 dir ^= 1
}
}
}
}
J - Mission Possible_线性规划
题意:
一个人从起点出发要走过距离D,每秒他会收到A点伤害(持续受到),他的初试血量为H=0,速度为V=0,生命恢复为R=0(每秒末回复R点生命值,不是持续的),最初购买一点血量、速度、生命回复的花费分别为GH、GV、GR。要求走到终点的过程中生命值大于等于0,问最小花费是多少?
题解:
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<sstream>
#include<vector>
#include<map>
#include<queue>
#include<stack>
#include<set>
#include<cmath>
#define up(i, x, y) for(int i = x; i <= y; i++)
#define down(i, x, y) for(int i = x; i >= y; i--)
#define MAXN ((int)1e5 + 10)
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
typedef long long ll;
ll D, A, G1, G2, G3;
int main()
{
int T; scanf("%d", &T);
up(case_, 1, T)
{
scanf("%lld %lld %lld %lld %lld", &D, &A, &G1, &G2, &G3);
printf("Case #%d: ", case_);
ll ans = INF;
up(V, 1, D) ans = min(ans, G2 * V + min( (ll)ceil( A * 1.0 * D / V ) * G1, A * (G1 + G3)) );
cout<<ans<<endl;
}
}