1. 程式人生 > 其它 >2020-2021 ICPC, NERC, Northern Eurasia Onsite BEIJ 題解

2020-2021 ICPC, NERC, Northern Eurasia Onsite BEIJ 題解

B. Button lock

題意:有 \(d\) 個 01 按鍵以及一個 reset 按鍵,你需要把所有題目給定的 \(n\) 個密碼全部表示一遍。只有按下 reset 按鍵後才能使所有 01 按鍵彈回。試使得按鍵次數最少。

做法:可以觀察到 \(ans = \sum_{u \in endvertex} (bitcount(u) + 1) - 1\),接下來要儘可能使 ans 最小。

二分圖匹配演算法可以保證其路徑數最少,接下來我們考慮二分圖匹配與匈牙利演算法。

先設左邊的點集為 S,右邊的點集為 T。假如做完了二分圖匹配,那麼若左邊點集 S 中有未匹配到的點,那麼說明這個點是一條路徑的結尾。這樣,答案變成了使得左邊點集 S 中未匹配的點的 1 的個數之和最少。

由於在匈牙利演算法中,我們依次列舉左側點集中的點,一個之前匹配到的點不可能再失配,而一個失配的點在之後還是失配。這樣,可以貪心地將所有密碼按照 1 的個數從大到小排序,然後再做匈牙利演算法。二分圖匹配演算法可以使路徑數最少,而上述排序使得失配的左側點的對答案的貢獻和儘可能小。

時間複雜度是 \(O(VE) = O(2^d3^d) = O(6^d)\)

上面少證明了一個東西:是否 ans 最小的時候滿足路徑數最少呢?反證法,若 ans 最小的時候路徑數不是最小的,那麼可想而知是在二分圖匹配中有一些更多的點失配了,在最大匹配的時候失配的點仍然失配;或是這個點雖然匹配上了,但是含有更多 1 的點失配了。這樣答案只會比滿足路徑數最少的條件時的答案更大。

#include <bits/stdc++.h>
using namespace std;

const int N = 1030;
const int D = 15;

#define IL inline
#define pb push_back
#define mk make_pair
#define fi first
#define se second

#define dbg1(x) cout << #x << " = " << x << ", "
#define dbg2(x) cout << #x << " = " << x << endl

int d, n;
int a[N];

IL bool in(int x, int y) {
  return (x & y) == x;
}

IL int bitcount(int x) {
  return x == 0 ? 0 : bitcount(x >> 1) + (x & 1);
}

struct Hungary {
  int n, m;
  int Left[N], Right[N];
  bool S[N], T[N];
  vector<int> G[N];

  IL void init(int n, int m) {
    this -> n = n; this -> m = m;
    fill(Left, Left + 1 + m, 0);
    fill(Right, Right + 1 + n, 0);
    fill(S, S+n+1, 0);
    fill(T, T+m+1, 0);
    for(int i=1;i<=n;i++) G[i].clear();
  }

  IL void AddEdge(int u, int v) {
    G[u].pb(v);
  }

  bool match(int u) {
    //dbg2(u);
    S[u] = true;
    for(auto v : G[u]) if(!T[v]) {
	//dbg1(u); dbg2(v);
	    T[v] = true;
	    if(Left[v] == 0 || match(Left[v])) {
	      Left[v] = u; Right[u] = v;
	      return true;
	    }
    }
    return false;
  }

  int gao() {
    int ans = 0;
    fill(Left, Left+1+m, 0);
    fill(Right, Right + 1 + n, 0);
    for(int i=1;i<=n;i++) {
      fill(T, T+1+m, 0);
      if(match(i)) ans++;
    }
    return ans;
  }

  vector<int> ans;

  int lb(int x) { return x & (-x);}
  
  void dfs(int u) {
    int v = Right[u];
    //dbg1(u); dbg2(v);
    if(v == 0) { ans.pb(0); return;}
    for(int val = (a[u] ^ a[v]), j = __lg(val); val; val -= (1 << j), j = __lg(val)) {
      //dbg1(val); dbg2(j);
      ans.pb(d - j);
    }
    dfs(v);
  }
  
  void solve() {
    //puts("wtf");
    int matchcnt = gao();
    //dbg1(matchcnt);
    for(int i=1;i<=n;i++) {
      if(Left[i] != 0) continue;
      for(int val = a[i], j = __lg(val); val; val -= (1 << j), j = __lg(val)) {
	//dbg1(i); dbg1(val); dbg2(j);
	      ans.pb(d - j);
      }
      dfs(i);
    }
    printf("%d\n", (int)ans.size()-1);
    for(int i=0;i<(int)ans.size()-1;i++) {
      if(ans[i] == 0) putchar('R');
      else printf("%d", ans[i] - 1);
      printf("%c", " \n"[i == (int)ans.size() - 2]);
    }
  }
};

Hungary lpr;

int main() {
  //freopen("B2.in", "r", stdin);
  //freopen("B2.out", "w", stdout);
  scanf("%d%d", &d, &n);
  lpr.init(n, n);
  for(int i=1;i<=n;i++) {
    for(int j=1;j<=d;j++) {
      int v; scanf("%1d", &v);
      a[i] = (a[i] << 1) | v;
    }
  }

  sort(a+1, a+1+n, [](int a, int b) { return bitcount(a) > bitcount(b);});

  //for(int i=1;i<=n;i++) {dbg1(i); dbg2(a[i]);}
  
  for(int i=1;i<=n;i++) {
    for(int j=1;j<i;j++) {
      if(!in(a[i], a[j])) continue;
      lpr.AddEdge(i, j);
    }
  }

  lpr.solve();
  
  return 0;
}

E. Equilibrium Point /\/\

題意:略

前置知識:多邊形的重心。每個三角形的

做法:可以發現合法括號序列的個數是卡特蘭數。在 n = 18 的時候,卡特蘭數為 477638700。那麼,是否可以使用類似卡特蘭數定義式的方式去做這個題呢?PS:\(f(n+1) = f(0)f(n) + f(1)f(n-1) + ... + f(n)f(0)\)

然後可以發現似乎有一些情況下重心是一樣的。也就是說可以把一些重心一樣的括號序列去掉。

由卡特蘭遞推式,我們考慮現在有 L 和 R 兩個括號序列,那麼接下來我們將它們合併起來,新的括號序列的重心是什麼樣子的呢?可以發現新的括號序列跟原來的括號序列所圍成的面積 \(m_L, m_R\),原來的重心 \((x_L, y_L), (x_R, y_R)\),以及 L 的長度 \(2i\) 相關。

\[m_{new} = m_L + m_R \\ x_{new} = x_L \frac{m_L}{m_{new}} + (x_R+2i) \frac{m_R}{m_{new}} \\ y_{new} = y_L \frac{m_L}{m_{new}} + y_R \frac{m_R}{m_{new}} \]

還有一種情況是假如現在只有 L 括號序列,那麼新的括號序列為 (L)

\[m_{new} = m_L + 2i - 1 \\ x_{new} = (x_L + 1) \frac{m_L}{m_{new}} + i \frac{2i-1}{m_{new}} \\ y_{new} = (y_L + 1) \frac{m_L}{m_{new}} + \frac{i - \frac{2}{3}}{2i-1} \cdot \frac{2i-1}{m_{new}} \]

也就是說,我們需要搞出來有 \(i\) 對括號的所有三元組 \((m, x, y)\) 。通過類似卡特蘭數定義式的列舉方法,可以預處理出來所有有 \(i\) 對括號情況下的三元組 \((m,x,y)\)。為了使得三元組唯一,請使用手寫雜湊表而不是STL,否則會TLE。這樣,跑完之後發現只枚舉了 79079257 次。即可解開此題。

另外,由於跑三元組的時候一方面由於精度問題,另一方面由於運算速度問題,最好避免使用浮點數。所以在維護三元組的時候,我們維護的東西其實是 \((m, mx, 3my)\),具體式子請自行帶入上方的式子推一推。

#include <bits/stdc++.h>
using namespace std;

#define mk make_pair
#define dbg1(x) cout << #x << " = " << x << ", "
#define dbg2(x) cout << #x << " = " << x << endl

typedef tuple<int, int, int> tiii;
typedef pair<tiii, tiii> pi6;
typedef long long ll;

const int mod = 10000007;
int cnt = 0;
int n;
double x, y;

struct Hash_table {
    int n, now;
    int hd[mod];
    int nxt[mod];
    int tim[mod];
    ll v[mod];
    void init() { n = 0; now++;}
    bool insert(ll val) {
        int s = val % mod;
        if(tim[s] != now) { tim[s] = now; hd[now] = 0;}
        for(int i=hd[s]; i!=0; i=nxt[i]) {
            if(v[i] == val) return false;
        }
        nxt[n + 1] = hd[s];
        v[n + 1] = val;
        hd[s] = ++n; 
        return true;
    }
}HASH;

vector<tuple<int, int, int> > a[20];


string solve(int i, tiii p) {
    if(i == 0) return "";
    for(auto b : a[i-1]) {
        int ml = get<0>(b), xl = get<1>(b), yl = get<2>(b);
        int mnew = ml + 2 * i - 1;
        int xnew = xl + ml + i * (2 * i - 1);
        int ynew = yl + 3 * ml + 3 * i - 2;
        if(tiii(mnew, xnew, ynew) == p) {
            return "(" + solve(i - 1, b) + ")";
        }
    }
    for(int j=1;j<i;j++) {
        for(auto b1 : a[j]) for(auto b2 : a[i-j]) {
            int ml = get<0>(b1), mr = get<0>(b2);
            int xl = get<1>(b1), xr = get<1>(b2);
            int yl = get<2>(b1), yr = get<2>(b2);
            int mnew = ml + mr;
            int xnew = xl + xr + 2 * j * mr;
            int ynew = yl + yr;
            if(tiii(mnew, xnew, ynew) == p) {
                return solve(j, b1) + solve(i - j, b2);
            }
        }
    }
    assert(0);
}

void add(int i, int mm, int xx, int yy) {
    long long v = mm * (1ll << 40ll) + xx * (1ll << 20ll) + yy;
    if(HASH.insert(v)) a[i].push_back(tiii(mm, xx, yy));
}

int main() {
#ifdef LOCAL
    freopen("E.in", "r", stdin);
    // freopen("E.out", "w", stdout);
#endif
    clock_t aa = clock();
    a[0].push_back(tiii(0, 0, 0));
    for(int i=1;i<=18;i++) {
        HASH.init();
        for(int j=1;j<i;j++) { // the size of left bracket sequence.
            for(auto b1 : a[j]) for(auto b2 : a[i-j]) {
                int ml = get<0>(b1), mr = get<0>(b2);
                int xl = get<1>(b1), xr = get<1>(b2);
                int yl = get<2>(b1), yr = get<2>(b2);
                int mnew = ml + mr;
                int xnew = xl + xr + 2 * j * mr;
                int ynew = yl + yr;
                add(i, mnew, xnew, ynew);
            }
            cnt += a[j].size() * a[i-j].size();
        }
        // ( sequence )
        for(auto b : a[i-1]) {
            int ml = get<0>(b), xl = get<1>(b), yl = get<2>(b);
            int mnew = ml + 2 * i - 1;
            int xnew = xl + ml + i * (2 * i - 1);
            int ynew = yl + 3 * ml + 3 * i - 2;
            add(i, mnew, xnew, ynew);
        }
        cnt += a[i-1].size();
        dbg1(i); dbg1(a[i].size()); dbg2(cnt);
    }
    clock_t b = clock();
    // cerr << "running " << b - aa << "ms\n";
    scanf("%d%lf%lf", &n, &x, &y);
    n /= 2;
    for(auto b : a[n]) {
        int bm = get<0>(b), bx = get<1>(b), by = get<2>(b);
        double xx = 1.0 * bx / bm, yy = 1.0 / 3.0 * by / bm;
        if(fabs(xx - x) + fabs(yy - y) <= 1e-8) {
            printf("%s\n", solve(n, b).c_str());
            return 0;
        }
    }
    return 0;
}

I. Is It Rated?

題意:略

做法:神奇的做法。演算法名稱叫做 Weighted Majority Algorithm,也可以百度天氣預測問題。但是百度到的證明的數學符號似乎都崩了,所以我看不懂這演算法是咋證明的……

程式碼實現中是給每一個人賦值一個他這一輪的可信權值,然後對 n 個人遍歷一遍得出這一輪選 0 的概率。然後再整一個 \([0,1]\) 的隨機數去跟上面的概率比對選 0/1.

這一輪結束後,如果對了那麼他的可信權值不用管,否則他的可信權值乘一個係數 \(\alpha\)。題解中說 \(\frac{1}{2} < \alpha < \frac{19}{20}\)。程式碼中取的 0.8。

#include <bits/stdc++.h>
using namespace std;

const int N = 1000 + 5;

int n, m;

char s[N];
long double a[N];

int main() {
    srand(time(0));
    scanf("%d%d", &n, &m);
    for(int i=1;i<=n;i++) a[i] = 1e18;
    for(int i=1;i<=m;i++) {
        scanf("%s", s + 1); getchar();
        long double tot0 = 0;
        long double tot = 0;
        for(int j=1;j<=n;j++) {
            if(s[j] == '0') tot0 += a[j];
            tot += a[j];
        }
        
        if(rand() * tot < tot0 * RAND_MAX) putchar('0');
        else putchar('1');
        putchar(10);
        fflush(stdout);

        char ans; ans = getchar(); 
        for(int j=1;j<=n;j++) if(s[j] != ans) {
            a[j] *= 0.8;
        }
    }
    return 0;
}

J. Japanese Game

題意:略

做法:

假如說現在有 profile 了,怎麼弄出來 mask?

顯然可以得出按照最左邊放置的放法,然後再得出按照最右邊放置的放法,然後看看哪些一直都是 # 的方塊仍然還是屬於一個大 # 塊的,這些方塊在 mask 中仍然是 #。

考慮 \(O(n^2)\) 做法。列舉按照最左邊放置的放法,在 mask 中顯示 # 的格子需要向左移動 \(i\) 格,而小於等於 \(i\) 格的 # 全部都被刪掉了需要加回來。這樣只需要分開考慮每一個 _ 塊怎麼填即可。

  • 如果 _ 在邊上且長度為1,無解。
  • 如果 __ 在中間且長度為 2,無解。
  • 對每一個 _ 塊加入長度為 1 或 2 的 # 塊,一邊加一邊判斷。

可以發現,若在 \(i \geq 4\) 時候有答案成立,一定有 \(i' = i - 2\) 有答案成立。那麼時間複雜度 \(O(n)\)

#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define dbg1(x) cout << #x << " = " << x << ", "
#define dbg2(x) cout << #x << " = " << x << endl

const int N = 100000 + 5;
const int inf = 1e9 + 7;

int n;
char ss[N];
char s[N];

struct Node {
    int l;
    char ch;
    Node(int l=0, char ch='\0'):l(l), ch(ch) {}
};

bool check(int lift) {
    for(int i=1;i<=n;i++) s[i] = ss[i];
    for(int i=1;i<=n;i++) if(s[i] == '#') {
        for(int j=1;j<=lift;j++) s[i-j] = '#';
    }
    s[n-lift+1] = '\0';
    vector<Node> a;
    for(int i=1, j=1; i<=n-lift; i++) {
        if(s[i] == s[j] && s[i+1] != s[i]) {
            int len;
            if(s[j] == '_') {
                if(i == j) { 
                    if(j == 1 || i == n - lift) return false;
                    else { j = i + 1; continue;}
                }
                if((j != 1 && i != n - lift) && i - j + 1 == 2) return false;
                len = i - j + (j == 1) + (i == n - lift);
            }
            else len = i - j + 1;
            a.pb(Node(len, s[j]));
            j = i + 1;
        }
    }
    vector<int> ans;
    for(int i=0;i<a.size();i++) {
        if(a[i].ch == '#') { ans.push_back(a[i].l); continue;}
        int len = a[i].l;
        for(int j=0;j<len/2-1;j++) {
            ans.pb(1);
            if(lift < 1) return false;
        }
        if(len % 2 == 1) {
            ans.pb(2);
            if(lift < 2) return false;
        }
        else { 
            ans.pb(1); 
            if(lift < 1) return false;
        }
    }
    printf("%d\n", (int)ans.size());
    for(int i=0;i<ans.size();i++) {
        printf("%d%c", ans[i], " \n"[i == (int)ans.size()-1]);
    }
    return true;
}

int main() {
#ifdef LOCAL
    freopen("J.in", "r", stdin);
#endif
    scanf("%s", ss + 1);
    n = strlen(ss + 1);
    int lift = inf;
    if(ss[1] == '#' || ss[n] == '#') lift = 0;
    for(int i=1, j=1;i<=n;i++) {
        if(ss[j] == '_' && ss[i] == '_' && i == n) {
            lift = min(lift, i - j + 1);
        }
        else if(ss[j] == '_' && ss[i] == '#') {
            if(j == 1) lift = min(lift, i - j);
            else lift = min(lift, i - j - 1);
        }
        if(ss[j] != ss[i]) j = i;
    }

    for(int k=0;k<=min(lift, 3); k++) {
        if(check(k)) return 0;
    }
    puts("-1");
    return 0;
}