前言:本次考试第二题炸了,删了暴力,后果很惨。。。
a 约数
题目描述
设K是一个正整数,设X是K的约数,且X不等于1也不等于K.
加了X后,K的值就变大了,你可以重复上面的步骤。例如K= 4,我们可以用上面的规则产生所有的非素数. 可以通过5次变化得到
24: 4->6->8->12->18->24.
现在给你两个整数N 和 M, 求最少需要多少次变化才能到从 N 变到 M. 如果没法从N变到M,输出-1.
输入格式
多组测试数据。
第一行:一个整数1<=ng<=5,表示有ng组测试数据。
每组测试数据格式如下:
一行:两个整数,N、M,空格分开。 4 <= N<=100000, N<=M<=100000.
输出格式
一个整数。求最少需要多少次变化才能到从 N 变到 M. 如果没法从N变到M,输出-1.
ng行,每行对应一组测试数据。
输入样例
2
4 24
4 576
输出样例
5 (题目的例子)
14(4->6->8->12->18->27->36->54->81->108->162->243->324->432->576)
解题思路(bfs)
这题这一看貌似数论,吓得我赶紧手推了半天,无果,发现N,M才100000而已,赶紧写了个bfs一次AC此水题。
看到签到题果真不能想太多,不要将简单的问题复杂化,而应将复杂的问题分解,使之简单化。
时间
代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <cstring>
#define N 100005
using namespace std;
int ng, n, m;
int head, tail;
int q[N], step[N];
int main(){
freopen("a.in", "r", stdin);
freopen("a.out", "w", stdout);
scanf("%d", &ng);
while(ng --){
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; i++) step[i] = -1;
q[head = tail = 0] = n;
step[n] = 0;
while(head <= tail){
int now = q[head++];
for(int i = 2; i * i <= now; i++){
if(now % i != 0) continue;
int next = now + i;
if(next <= m && step[next] == -1){
q[++tail] = next;
step[next] = step[now] + 1;
}
if(i != now / i){
next = now + now / i;
if(next <= m && step[next] == -1){
q[++tail] = next;
step[next] = step[now] + 1;
}
}
}
}
printf("%d\n", step[m]);
}
return 0;
}
b 小偷与警察
题目描述
为帮助捕获在逃的犯人, 警局引进了一套新计算机系统. 系统覆盖了N 个城市,有E条双向的道路。城市标号为1 到N. 犯人经常从一个城市逃到另外一个城市. 所以警察想知道应该在哪里设置障碍去抓犯人.计算机系统需要回答下面两种类型的问题:
1. 考虑城市A 、B; 如果把连接城市G1和G2的那条公路切断,逃犯还能从城市A逃到城市B吗?
2. 考虑三个城市A、B 、C. 如果把城市C*(则不能从其他进入城市C),逃犯还能从城市A逃到城市B吗?
你的任务是帮计算机系统回答这些提问。(一开始,任意两个城市都是可以相互到达的).
输入格式
- 第一行: 两个整数N 、 E (2 <= N <= 100 000, 1 <= E <= 500 000),表示城市的数量和道路的数量.
- 第 2..E+1行: 两个不同整数A和B,表示城市A和城市B之间有一条公路,任意两个城市最多只有一条公路。
- 第 E+2行:一个整数 Q (1 <= Q <= 300 000), 表示有Q个提问。
- 第 E+3..E+Q+2行: 这Q行,每行有4个或5个整数. 第一整数表示提问的类型(1或2). 如果是提问类型是1, 那么本行后面有4个整数: A、B、G1、G2 ,参数表示的意义题目已经说过,其中A和B不会相同,G1和G2之间肯定有一条公路. 如果提问类型是2,那么本行后面有3个整数: A、B 、C. ( A、B 、C都不相同,意义上面已经说过。)
输出格式
- 第1..Q行: 每行输出yes 或 no .
输入样例
13 15
1 2
2 3
3 5
2 4
4 6
2 6
1 4
1 7
7 8
7 9
7 10
8 11
8 12
9 12
12 13
5
1 5 13 1 2
1 6 2 1 4
1 13 6 7 8
2 13 6 7
2 13 6 8
输出样例
yes
yes
yes
no
yes
解题思路(dfs树+LCA(树上倍增))
这是一道好题。考试时乱写了个Tarjan+LCA,没有考虑到割顶的情况,就爆9了。都怪我写了暴力又删了,哪来的自信啊!
然后我就发现这题跟割顶和桥有点关系。
(以下皆为口胡)
割顶の定义:割掉一个点A 后,图连通分量个数增加,则A 为割顶。
割顶の性质:记点i 的dfs序为dfn[i] ,low[i] 为从点i 通过一条返祖边连回的最早的dfn 。若A 为割顶,当且仅当其存在一个儿子v 使得low[v]>=dfn[A] 。
割顶の求法:
①枚举每个点删掉,统计连通分量个数。
②据性质线性求(根处要特判)桥の定义:割掉一条边
L 后,图连通分量个数增加,则L 为桥。
桥の性质:记深度为dep[i] ,若边A−B 为桥(dep[A]<dep[B] ),当且仅当low[B]>dfn[A] 。类比割顶,我们发现桥的上方必然有一个割顶。换而言之,存在割顶是其下方存在桥的必要条件。
桥の求法:类比割顶。
我们考虑先对此无向图做一遍dfs,求得其dfs树。由于无向图,我们只有树边与返祖边。如果是有向图的dfs树是有树边、返祖边、横叉边和正向边的。
我一开始是想着求双联通分量然后什么缩环之类的,其实并不用。
考虑割一个点,如果在起点与终点的路径上的话(求LCA),才可能有影响。然后此点必然是割顶(必要条件)。然后用树上倍增从一个点跳过去其下方的节点,看看能不能跑掉(即通过返祖边)。这个用类似于求割顶的方法(图我就不画了)。
然后这里有一个特例,如果要割的点在LCA的话,要两边都跳以下,一端被割就不行了。单次时间
考虑割一条边,这个简单多了,比上一个好想。直接判断边在路径上吗,不在一定无影响。否则判断是否为桥即可。
这题亦可不用LCA用(LCT×)。我们发现能否连通跟两点是否在一棵子树有关系,于是直接利用dfs序判断就可以。这就不用LCA了(其实也一样)。然后判断割点时还是要树上倍增一下,或者二分+链表(vector),并没有好写多少。
代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define N 100010
#define M 500010
using namespace std;
int n, m, Q, cur, Tim, child;
int head_p[N];
int dep[N], dfn[N], low[N];
int f[25][N];
struct Tadj{int next, obj;} Edg[M<<1];
void Init(){
cur = -1;
memset(head_p, -1, sizeof(head_p));
memset(dfn, 0, sizeof(dfn));
}
void Insert(int a, int b){
cur ++;
Edg[cur].next = head_p[a];
Edg[cur].obj = b;
head_p[a] = cur;
}
void dfs(int root, int fa){
dfn[root] = low[root] = ++ Tim;
dep[root] = dep[fa] + 1;
f[0][root] = fa;
for(int i = head_p[root]; ~ i; i = Edg[i].next){
int v = Edg[i].obj;
if(!dfn[v]){
if(root == 1) child ++;
dfs(v, root);
low[root] = min(low[root], low[v]);
}
else if(dfn[v] < dfn[root] && v != fa) low[root] = min(low[root], dfn[v]);
}
}
void ycl(){
dfs(1, 0);
for(int i = 1; i <= 20; i++)
for(int j = 1; j <= n; j++)
f[i][j] = f[i-1][f[i-1][j]];
}
int Getlca(int x, int y){
if(dep[x] > dep[y]) swap(x, y);
for(int i = 20; i >= 0; i--)
if(dep[f[i][y]] >= dep[x]) y = f[i][y];
if(x == y) return x;
for(int i = 20; i >= 0; i--)
if(f[i][x] != f[i][y]){
x = f[i][x];
y = f[i][y];
}
return f[0][x];
}
bool Onpath(int A, int B, int C){
int L1 = Getlca(A, B), L2 = Getlca(A, C), L3 = Getlca(B, C);
if(dfn[C] < dfn[L1]) return false;
if(L2 != C && L3 != C) return false;
return true;
}
void Jump(int A, int B, int C){
int L1 = Getlca(A, B), L2 = Getlca(A, C), L3 = Getlca(B, C);
int y;
if(L2 == C){
y = A;
for(int i = 20; i >= 0; i--)
if(dep[f[i][y]] > dep[C]) y = f[i][y];
if(!(low[y] < dfn[C] || (C == 1 && child == 1))){
printf("no\n");
return;
}
}
if(L3 == C){
y = B;
for(int i = 20; i >= 0; i--)
if(dep[f[i][y]] > dep[C]) y = f[i][y];
if(!(low[y] < dfn[C] || (C == 1 && child == 1))){
printf("no\n");
return;
}
}
printf("yes\n");
}
int main(){
freopen("b.in", "r", stdin);
freopen("b.out", "w", stdout);
scanf("%d%d", &n, &m);
Init();
int a, b;
for(int i = 1; i <= m; i++){
scanf("%d%d", &a, &b);
Insert(a, b);
Insert(b, a);
}
ycl();
scanf("%d", &Q);
int sign, A, B, C, G1, G2;
for(int i = 1; i <= Q; i++){
scanf("%d", &sign);
if(sign == 1){
scanf("%d%d%d%d", &A, &B, &G1, &G2);
if(!Onpath(A, B, G1) || !Onpath(A, B, G2)) printf("yes\n");
else{
if(dfn[G1] > dfn[G2]) swap(G1, G2);
if(low[G2] > dfn[G1]) printf("no\n");
else printf("yes\n");
}
}
else{
scanf("%d%d%d", &A, &B, &C);
if(!Onpath(A, B, C)) printf("yes\n");
else Jump(A, B, C);
}
}
return 0;
}
c 圆桌会议
题目描述
有N个人顺时针围在一圆桌上开会,他们对身高很敏感. 因此决定想使得任意相邻的两人的身高差距最大值最小. 如果答案不唯一,输出字典序最小的排列,指的是身高的排列.
输入格式
多组测试数据。第一行:一个整数ng, 1 <= ng <= 5. 表示有ng组测试数据。
每组测试数据格式如下:
第一行: 一个整数N, 3 <= N <= 50
第二行, 有个N整数, 第i个整数表示第i个人的身高hi, 1<=hi<=1000. 按顺指针给出N个人的身高. 空格分开。
输出格式
字典序最小的身高序列,同时满足相邻的两人的身高差距最大值最小。共ng行,每行对应一组输入数据。
输入样例
2
5
1 3 4 5 7
4
1 2 3 4
输出样例
1 3 5 7 4
1 2 4 3
解题思路(二分+(贪心/网络流)/双路dp)
排序后很明显的二分,然后套个贪心乱搞。
很明显答案一定是个单峰,类似于上一座山又下来。
于是确定最小值和最大值,之间就有两条路径。
为了能使二分到的答案满足,贪心地让上山经过的节点尽可能少,多的留给下山才可能从山峰走下。这里判一下能否登顶并下去。
然后得到答案后,考虑字典序问题。由于我们二分时是使山顶前的节点尽可能少,于是山峰尽可能靠前。而要求字典序最小要求山峰尽可能靠后。于是在二分时保存答案最后倒序输出就行了。
至于贪心的严谨证明,就是个麻烦活了。但我们还是能够理解并举几个例子检验的。提高贪心的证明能力需要提高数学证明的功底,推翻贪心需要能举出反例的脑洞。
然后深入思考,为什么选最后下山的路径不是从山顶尽快下去而是从山底尽快上去呢?这是不同的。因为从山顶尽快下去是遇到节点就选,然后到达最后,这与从山顶相比是将大的值放在了前面,明显我们要是字典序小就要将大的值放后面。所以这个从先上山变成后下山的想法正确的。换而言之,我们无法交换两条路径上的值使答案更优,而一条路上的值也不能单独改变,所以这是有道理的。
说一句废话:都已AC的贪心你不能说它哪里不对吧。。。
另两种方法:二分后用网络流检验,看看能否跑出两条路径就行了。重点在与字典序最小,就枚举看看每个点能不能放,再合并点跑网络流检验就行了。很强大的一种方法。
双路dp就不用二分,直接记状态(记忆化搜索),然后记抛物线的开口下的两条支路的结尾的答案为
其实算答案直接将第一小放左边,第二小放右边,累次放下去一定最优。然后各种方法调整字典序(交换之类,或者直接贪心地放)就可以得到答案。
总之二分+贪心就够好了。所有方法调整字典序的方法都是与算答案的方法类似。
代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#define oo 0x7fffffff
#define N 60
using namespace std;
int ng, n, last, h[N];
bool vis[N], rec[N];
bool Judge(int dis){
for(int i = 1; i <= n; i++) vis[i] = false;
last = 1; vis[1] = true;
for(int i = 2; i <= n; i++)
if(h[i] - h[last] > dis){
last = i - 1;
if(vis[last]) break;
vis[last] = true;
}
if(h[n] - h[last] > dis) return false;
vis[1] = vis[n] = false;
for(int i = 1; i < n; i++)
for(int j = i+1; j <= n; j++){
if(vis[i] || vis[j]) continue;
if(h[j] - h[i] > dis) return false;
break;
}
for(int i = 1; i <= n; i++) rec[i] = vis[i];
return true;
}
int main(){
freopen("c.in", "r", stdin);
freopen("c.out", "w", stdout);
scanf("%d", &ng);
while(ng --){
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
sort(h+1, h+n+1);
int L = -1, R = h[n] - h[1];
while(L + 1 < R){
int mid = (L + R) >> 1;
if(Judge(mid)) R = mid;
else L = mid;
}
if(!Judge(R)) printf("My hope is the world peace");
for(int i = 1; i <= n; i++) if(!rec[i]) printf("%d ", h[i]);
for(int i = n; i > 0; i--) if(rec[i]) printf("%d ", h[i]);
printf("\n");
}
return 0;
}
总结
提高码代码的准确度与稳定性,思考时间不能少,要学会水分和暴力。
还能说什么呢,加油吧,没有天赋,到达终点只能靠努力。
但求无悔。
还会有人让你睡不着,还能为某人燃烧 。