2019 ICPC NCPU 簡易講評
===
###### tags: `ICPC` `NCPU` `NCTU` `NCPU2019`
[正賽題本](https://drive.google.com/open?id=1RqS5t_8qa77T2Tcxvjo6vqgNQkgkgdY7)
[參考測資](https://drive.google.com/open?id=1v6L6O45ILNZ5TJixaQgytggOS-O1D15q)
先貼 Sample code ,有空再來寫點講評。我的專長是胡說八道,如果你認為講評亂寫,那很可能是正常的。如果有疑問想要討論,可以來信 mzshieh@nctu.edu.tw ,我再來研究該如何進行。
工商時間一:如果比賽可以寫 Kotlin 又不會 TLE,那何苦寫 JAVA。
工商時間二:其實也可以用 Python ,但 Python 太夯又在某些硬核題目一定會 TLE ,在規則修改之前,請大家不要立志當個 Python 戰士。
工商時間三:如果有菜雞發現自己被多測資 EOF 結尾坑,請參考 Problem C 跟 Problem E 的 sample code。
## Problam A
計算出輸入的數字 $n$ 能夠整除的最小 $k$ 階乘 ($k!$)。保證 $k$ 在 $1$ 到 $12$ 之間,一個一個判斷即可。命題時有確保計算過程中不會超過 32-bit 有號整數,算是相當友善的一題。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
fun main() {
val m = nextInt()
for (x in 0 until m) {
val n = nextInt()
var fact = 1
for (i in 1..12) {
fact *= i
if (fact % n == 0) {
println("$i")
break
}
}
}
}
```
## Problem B
計算 $b, b+1, \ldots, e$ 用二進位表示法時總共有多少個 $1$,如果你會使用像是 Java/Kotlin 的 `java.lang.Integer.bitCount()` 或是 C++ 的 `__builtin_popcount()` 則寫起來會非常快。當然如何快速的計算出一個整數的 set bit 有多少是一個有趣的議題也是常常被拿來面試的題目,有興趣的同學可以看一下 [Hamming weight](https://en.wikipedia.org/wiki/Hamming_weight) 的維基百科。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
fun main() {
val m = nextInt()
for (x in 1..m) {
val (b, e) = nextInts()
var ans = 0
for (i in b..e) {
ans += java.lang.Integer.bitCount(i)
}
println("$ans")
}
}
```
註一:NCPU參賽者都對了,作為命題者感謝大家,我希望以後我能出有鑑別度的題目。原本打算要出的題目規格是 $1\le b\le e \le 10^{18}$,有興趣的同學可以想一下這樣要怎麼算。
註二:或許有些人會想要寫的非常短,像是下面這樣。現代的 C++/Java/Kotlin 都有一些方便資料處理的 functional language 類型的寫法。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
fun main() = (1..nextInt()).forEach {
val (b, e) = nextInts()
println((b..e).map{java.lang.Integer.bitCount(it)}.sum())
}
```
## Problem C
本題最重要的是發現操作 4 是一個幌子,只需要紀錄是否有翻轉,剩餘的就是考實做能力了。各種偷吃步都歡迎大家用,但是取巧過頭未必好除錯。本次的測試資料使用 array-based 的結構來模擬是有機會在時限內通過,但在國際賽時不建議各位這麼大膽,要知道複雜度分析套下去,很可能會要 $O(nm)$ 的時間。正確的作法是使用 linked-list ,範例程式碼即為一種實現的方法。
```cpp=
#include<bits/stdc++.h>
using namespace std;
struct node{
struct node *nxt, *prv;
long long v;
}pos[100002];
inline node* cut(node *p)
{
// cut a node from the list via its pointer
// return value is an implementation trick
p->nxt->prv = p->prv;
p->prv->nxt = p->nxt;
p->nxt = p->prv = NULL;
return p;
}
inline node* insert(node *p, node *q)
{
// idea: always insert q as a successor (on the right)
q->nxt = p->nxt;
p->nxt->prv = q;
q->prv = p;
p->nxt = q;
return q;
}
int main()
{
int n, m, scale=0;
while(cin >> n >> m)
{
scale += n+m;
for(int i = 0; i <= n+1; i++)
{
// dummy head & tail
pos[i].nxt = (i<=n)?pos+i+1:NULL;
pos[i].prv = (i>0)?pos+i-1:NULL;
pos[i].v = (i<=n and i > 0)?i:0;
}
bool rev = false;
for(int i = 0; i < m; i++)
{
int op, x, y;
cin >> op;
if(op==4){
rev = !rev;
continue;
}
cin >> x >> y;
node *lx=pos[x].prv, *ly = pos[y].prv;
if(op==3){
node *lx=pos[x].prv, *ly = pos[y].prv;
if(lx==pos+y){
insert(ly,cut(pos+x));
}
else if(ly==pos+x){
insert(lx,cut(pos+y));
}
else{
insert(lx,cut(pos+y));
insert(ly,cut(pos+x));
}
}
else if(op==(rev?1:2)){ // x to the right or reverse left of y
if(lx==pos+y) continue;
insert(pos+y,cut(pos+x));
}
else{ // x to the left or reverse right of y
if(ly==pos+x) continue;
insert(ly,cut(pos+x));
}
}
long long ans = 0;
if(rev)
{
for(node *p = pos[n+1].prv; p; p = p->prv)
{
ans += p->v;
p = p->prv;
if(!p) break;
}
}
else
{
for(node *p = pos[0].nxt; p; p = p->nxt)
{
ans += p->v;
p = p->nxt;
if(!p) break;
}
}
cout << ans << endl;
}
return 0;
}
```
不知道該怎樣說 Kotlin 的 `null` 與 `!!`
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
data class Node(var value: Int, var next: Node? = null,var prev: Node? = null){
fun cut(): Node {
this.next!!.prev = this.prev
this.prev!!.next = this.next
this.prev = null
this.next = null
return this
}
fun insert(p: Node) {
p.prev = this
p.next = this.next
this.next!!.prev = p
this.next = p
}
}
fun solve() {
val (n, m) = nextInts()
var rev = false
var node = (0..(n+1)).map{Node(it%(n+1))}
(0..n).map{node[it+1].prev = node[it]}
(0..n).map{node[it].next = node[it+1]}
for (opCnt in 1..m) {
val op = nextInts()
if (op[0] == 4) {
rev = !rev
continue
}
var (p, x, y) = op
var lx = node[x].prev!!
var ly = node[y].prev!!
if (rev && p in 1..2) p = 3 - p
when (p) {
3 -> {
if (ly!=node[x]) ly.insert(node[x].cut())
if (lx!=node[y]) lx.insert(node[y].cut())
}
2 -> node[y].insert(node[x].cut())
1 -> if (ly!=node[x]) ly.insert(node[x].cut())
}
}
var ans = 0L
var iter: Node? = node[0]
if (!rev || n%2 == 1) iter = iter!!.next
while (iter!=null && iter.next != null) {
ans += iter.value
iter = iter.next!!.next
}
println(ans)
}
fun main() {
while (true) {
try {
solve()
}
catch (e: NullPointerException) {
break
}
}
}
```
## Problem D
總之就是把資料讀進來,去除不可用的資料,其餘做成一些可比較大小的資料。排序,找前三個。如果找不到前三個,那就有幾個算幾個。最後,依照順序印出來。在這種時候,現代 C++/Java/Kotlin 都提供了一些相對 functional 一點的寫法。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
data class AP(val macAddr: String, val rssi: Int): Comparable<AP> {
override fun compareTo(other: AP): Int {
if(this.rssi == other.rssi)
return this.macAddr.compareTo(other.macAddr)
return other.rssi - this.rssi
}
}
fun main() {
val ncases = nextInt()
for (case_i in 1..ncases) {
val n = nextInt()
val aps = (1..n).map{nextToks()}
.filter{it.last().toInt()<=1000}
.map{AP(it[0],it[1].toInt())}
aps.sorted().take(3).forEach{
val (mac, rssi) = it
println("$mac $rssi")
}
}
}
```
## Problem E
這一題其實是個閱讀測驗,每次做一層,然後往下面填下去。請認真觀察每一個格子會貢獻他的值給下方三個格子,編號邊好沒煩惱。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
fun solve(n: Int) {
var ans: Array<Array<LongArray>> = Array(n+1) {
Array(n+1){ LongArray(n+1) {0L} }
}
ans[0][0][0] = 1L
for (i in 0 until n)
for (j in 0..i)
for (k in 0..i) {
ans[i+1][j][k] += ans[i][j][k]
ans[i+1][j+1][k+1] += ans[i][j][k]
ans[i+1][j+1][k] += ans[i][j][k]
}
for (i in 0..n)
println(ans[n][i].take(i+1).joinToString(separator=" "))
}
fun main() {
try {
while (true) {
solve(nextInt())
}
}
catch (e: NullPointerException) {
}
}
```
## Problem F
這一題可以使用遞迴或是 stack 來做。範例程式碼是用遞迴,但通常要恰當的使用 Stack 效率才會好。由於測資規模的關係,可以不用很在意實做上的複雜度。註:這一題我驗題時沒寫,一開始貼的程式碼是不知道是誰寫的,只好重新寫一份 Kotlin。不過這題相信用 Python 會更簡短。
```kotlin=
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
private fun extract(s: String): String {
if ('[' in s) {
val l = s.indexOf('[')
var acc = 1
for (r in (l+1)..s.length) {
if (s[r] == '[') acc += 1
if (s[r] == ']') acc -= 1
if (acc == 0) {
return s.substring(0,l-1)+
extract(s.substring(l+1,r))
.repeat(s.substring(l-1,l).toInt())+
extract(s.substring(r+1))
}
}
}
return s
}
fun main() {
(1..nextInt()).map{nextLine()}.map{extract(it)}.map{println(it)}
}
```
## Problem G
先跟各位道歉,這題我已經忘記我當初在寫什麼了。這個題目其實規模大的時候需要點想法,但實際上規模很小,只需要討論幾件事情就好了:
1. 圖是否連通,沒有就 `n` 了
2. 圖如果連通,那就檢查邊的數量。邊數比點數少一,就是棵樹,那也是 `n`。邊同點數,那就是剛好有一個 cycle,也是 `n`。
3. 邊數比點數多 $k>0$ ,那至少會有 $k+1$ 個 Cycle:想像一個 Spanning tree,任何不在 Spanning tree 的邊都會製造出一個 cycle 且其上的邊都是屬於那個 Spanning tree 的。$k>1$ 的也就結案,是 `y: there are at least three cycles`
4. 剩下來的就是邊數比點數多一個邊需要判斷。因測資規模限制,這頂多只有 11 條邊,因此窮舉所有的可能性來判斷是否剛好有兩個 cycle 即可決定答案。註:如果圖比較大,就要檢查兩條不在 Spanning tree 上的邊是不是可以形成某 cycle 上的兩邊,由於 Spanning tree 上兩點之間的路徑唯一,要快速檢查並不難的。
```cpp=
#include<bits/stdc++.h>
using namespace std;
int fin(vector<int> &p, int x)
{
if(p[x]==-1) return x;
return p[x] = fin(p,p[x]);
}
int cc(int n, int m, const vector<pair<int, int>> &e)
{
vector<int> p(n,-1);
for(auto &x: e)
{
int u = fin(p,x.first), v = fin(p,x.second);
if(u!=v) p[u]=v;
}
int cnt=0;
for(int i = 0; i < n; i++)
if(p[i]==-1) cnt++;
return cnt;
}
bool connected(int n, int m, const vector<pair<int, int>> &e)
{
return cc(n,m,e)==1;
}
bool verify(int n, int m, const vector<pair<int, int>> &e)
{
int cnt = 0;
for(int i = 1; i < (1<<m); i++)
{
vector<int> deg(n,0);
vector<pair<int,int>> f;
for(int j = 0; j < m; j++)
{
if((1<<j)&i)
{
deg[e[j].first]++;
deg[e[j].second]++;
f.push_back(e[j]);
}
}
bool all_deg_2_or_0=true;
int deg_2_cnt = 0;
for(int j = 0; j < n; j++)
if(deg[j]!=0 && deg[j]!=2)
all_deg_2_or_0=false;
else if(deg[j]==2) deg_2_cnt++;
if(!all_deg_2_or_0) continue;
if(cc(n,f.size(),f)==n-deg_2_cnt+1) cnt++;
}
return cnt >= 3;
}
void solve()
{
vector<pair<int, int>> e;
int n, m, u, v;
scanf("%d%d",&n,&m);
for(int i = 0; i < m; i++)
{
scanf("%d%d",&u,&v);
e.push_back(make_pair(u-1,v-1));
}
if(!connected(n,m,e) || m<=n) puts("n");
else if(m>n+1 || verify(n,m,e)) puts("y: there are at least three cycles");
else puts("y");
}
int main()
{
int ncases;
scanf("%d",&ncases);
while(ncases--) solve();
return 0;
}
```
## Problem H
這一題前面三個先特例處理。其餘的先觀察當 $\max(|x|,|y|)=k$ 時,總共會有 $4\phi(k)$ 個 $\frac{y}{x}$ 是題目說的 rational number。如果你看完錢一句話,不知道我在寫什麼,那很可能是你不認識 [Euler's Totient function](https://en.wikipedia.org/wiki/Euler%27s_totient_function),先請你看個參考資料,這個上離散數學或是密碼學時很可能會學到。有很多數學的結論可以幫助各位同學計算 $\phi(k)$,如利用相關性質改寫 [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) 可以在 $O(n\log n)$ 算出來。因此再針對要求的問題做一次 prefix sums來計算出有多少個 rational number 是 $\max(|x|,|y|)<k$ 的,並跑至多 $O(k)$ 次 GCD 計算驗出有幾個 $\max(|x|,|y|)=k$ 的 rational number 排在輸入測資的前面。總體耗時約 $O(n\log n)$ (在 Word-RAM model 下)。
```cpp=
#include<bits/stdc++.h>
using namespace std;
long long phi[1000001] = {0LL,1LL};
int witness[1000001] = {0};
long long acc[1000001] = {0,3};
long long GCD(long long x, long long y)
{
//assume 0<=x<=y
return x?GCD(y%x,x):y;
}
long long gcd(long long x, long long y)
{
return GCD(abs(x),abs(y));
}
void solve()
{
long long x, y, z;
scanf("%lld%lld",&y,&x);
z = max(abs(x),abs(y));
if(x<=0) printf("0\n");
else if(gcd(abs(y),abs(x))!=1) printf("0\n");
else if(max(abs(x),abs(y))==1) printf("%lld\n",2+y);
else
{
long long ans = acc[z-1];
if(y<0 and x+y < 0)
{
for(int i = 1; i <= x; i++)
if(gcd(i,z)==1) ans++;
}
else if(y<0 and x+y >0)
{
ans += phi[z];
for(int i = -z+1; i <= y; i++)
if(gcd(i,z)==1) ans++;
}
else if(y>0 and x-y >0)
{
ans += 2*phi[z];
for(int i = 1; i <= y; i++)
if(gcd(i,z)==1) ans++;
}
else
{
ans += 3*phi[z];
for(int i = z-1; i >= x; i--)
if(gcd(i,z)==1) ans++;
}
printf("%lld\n",ans);
}
}
int main()
{
for(int i = 2; i <= 1000000; i++) phi[i]=i;
for(int i = 2; i <= 1000000; i++)
if(witness[i]==0)
{
phi[i] = i-1;
for(long long j = i+i; j <= 1000000; j+=i)
witness[j] = i, phi[j] /= i, phi[j]*=i-1;
}
for(int i = 2; i <= 1000000; i++)
acc[i] = acc[i-1]+phi[i]*4;
int ncases;
scanf("%d",&ncases);
while(ncases--) solve();
return 0;
}
```
補個 Kotlin 版本。
```kotlin=
import kotlin.math.*
private fun nextLine() = readLine()!!
private fun nextInt() = nextLine().toInt()
private fun nextToks() = nextLine().split(" ")
private fun nextInts() = nextToks().map{it.toInt()}
const val MAX = 1000001
var phi = IntArray(MAX+1){it}
var notPrime = BooleanArray(MAX+1){false}
var acc = LongArray(MAX+1){0L}
fun gcd(x: Int, y: Int): Int {
return if (x==0) y else gcd(y%x, x)
}
fun solve() {
val (y,x) = nextInts()
val z = max(abs(x),abs(y))
if (x<=0) println(0)
else if (gcd(abs(x),abs(y))!=1) println(0)
else if (z==1) println(2+y)
else {
var ans = acc[z-1]
if (y<0 && x+y<0) {
for (i in 1..x) if (gcd(i,z)==1) ans += 1
}
else if (y<0 && x+y>0) {
ans += phi[z]
for (i in (1-z)..y) if (gcd(-i,z)==1) ans += 1
}
else if (y>0 && x-y>0) {
ans += 2*phi[z]
for (i in 1..y) if (gcd(i,z)==1) ans += 1
}
else {
ans += 3*phi[z]
for (i in z downTo x) if (gcd(i,z)==1) ans += 1
}
println(ans)
}
}
fun main() {
acc[1] = 3
for (i in 2..MAX) {
if (notPrime[i] == false) {
for (j in i..MAX step i) {
notPrime[j] = true
phi[j] /= i
phi[j] *= i-1
}
}
acc[i] = phi[i]*4 + acc[i-1]
}
(1..nextInt()).forEach{solve()}
}
```