NOIP2017模拟赛(4) 总结
前言:本次考試第二題炸了,刪了暴力,后果很慘。。。
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此水題。
看到簽到題果真不能想太多,不要將簡單的問題復雜化,而應將復雜的問題分解,使之簡單化。
時間 O(ng?n?n√) 。
代碼
#include <iostream> #include <cstdio> #include <cstdlib> #include <algorithm> #include <cmath> #include <cstring> #define N 100005using 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的話,要兩邊都跳以下,一端被割就不行了。單次時間 O(log?n) 。(ps:這個地方我錯了好久QAQ)
考慮割一條邊,這個簡單多了,比上一個好想。直接判斷邊在路徑上嗎,不在一定無影響。否則判斷是否為橋即可。
這題亦可不用LCA用(LCT×)。我們發現能否連通跟兩點是否在一棵子樹有關系,于是直接利用dfs序判斷就可以。這就不用LCA了(其實也一樣)。然后判斷割點時還是要樹上倍增一下,或者二分+鏈表(vector),并沒有好寫多少。
代碼
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <algorithm> #define N 100010 #define M 500010using 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就不用二分,直接記狀態(記憶化搜索),然后記拋物線的開口下的兩條支路的結尾的答案為 f[i][j] ,然后枚舉下一個點放哪里轉移。然后得到答案,字典序也用了跟算答案有關的方法。直接枚舉第 i <script type="math/tex" id="MathJax-Element-20">i</script>個是誰,然后查表看看滿不滿足,還有其他細節的話我也不清楚。
其實算答案直接將第一小放左邊,第二小放右邊,累次放下去一定最優。然后各種方法調整字典序(交換之類,或者直接貪心地放)就可以得到答案。
總之二分+貪心就夠好了。所有方法調整字典序的方法都是與算答案的方法類似。
代碼
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <algorithm> #define oo 0x7fffffff #define N 60using 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; }總結
提高碼代碼的準確度與穩定性,思考時間不能少,要學會水分和暴力。
還能說什么呢,加油吧,沒有天賦,到達終點只能靠努力。
但求無悔。
還會有人讓你睡不著,還能為某人燃燒 。
總結
以上是生活随笔為你收集整理的NOIP2017模拟赛(4) 总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C#语言实例源码系列-自定义ListBo
- 下一篇: 励志