10月20日模擬賽題解

10月20日模擬賽題解

A 紙牌

Description

桌面上有 \(n\) 張紙牌,每張紙牌的正反兩面各寫着一個整數,初始時正面朝上。現在要求你翻動最少的紙牌,使得朝上的數字中最少有一半的數字是相同的,或判斷無解。

Limitations

\(1 \leq n \leq 3 \times 10^5\),所有輸入數據都是不大於 \(10^9\) 的非負整數。

Solution

簽到題,注意到因爲要求至少有一半的數字相同,並且一共只有 \(2n\) 個數字,所以最終可以作爲相同數字的數不超過 \(4\) 個。用 map/hash 隨便維護一下出現次數,然後枚舉超過一半的數字,暴力判斷即可。

Code

#include <cstdio>
#include <map>
#include <vector>

const int maxn = 300005;

int n, ans = maxn, dn;
int a[maxn], b[maxn];
std::map<int, int> oc;
std::vector<int> ansv;

int main() {
  freopen("card.in", "r", stdin);
  freopen("card.out", "w", stdout);
  qr(n); dn = (n >> 1) + (n & 1);
  for (int i = 1; i <= n; ++i) {
    qr(a[i]); qr(b[i]);
    if (a[i] != b[i]) {
      if (++oc[a[i]] == dn) {
        ansv.push_back(a[i]);
      }
      if (++oc[b[i]] == dn) {
        ansv.push_back(b[i]);
      }
    } else {
      if (++oc[a[i]] == dn) {
        ansv.push_back(a[i]);
      }
    }
  }
  if (ansv.size() == 0) {
    puts("Impossible");
    return 0;
  }
  for (auto v : ansv) {
    int cnt = 0;
    for (int i = 1; i <= n; ++i) {
      cnt += a[i] == v;
    }
    ans = std::min(ans, dn - cnt);
  }
  printf("%d\n", std::max(0, ans));
}

B 後綴樹組

Description

給定一個長度爲 \(n\) 的字符串,對每個位置 \(i\) ,取它和它後面 \((m - 1)\) 個字符共 \(m\) 個字符作爲第 \(i\) 個子串。(如果到達結尾,則子串長度爲 \(n - i + 1\))。現在將這 \(n\) 個子串按照第一個字符所在的位置排成一排,要將他們按照字典序排序,每次只能交換相鄰字符串,求最少交換次數。

Limitations

對於全部的數據,\(1 \leq m \leq n \leq 50000\),字符串只含小寫字母

對於前 \(60\%\) 的數據,\(1 \leq n \leq 5000\)

另有 \(10\%\) 的數據, \(1 \leq m \leq 5\)

另有 \(10\%\) 的數據,字符串隨機生成。

Solution

首先的結論是,將一個序列按照不降序排序且只能交換相鄰兩項,則最優的交換次數是這個序列的逆序對數。證明上可以考慮先將最大的元素移動到序列末尾,所需要的移動次數是該最大元素所貢獻的逆序對個數,然後去掉序列末尾元素,對剩下的序列繼續排序,以此做數學歸納即可。

因此只要知道每個子串的字典序排名,我們就可以 \(O(n \log n)\) 的求出答案。

考慮前 \(60\%\) 的數據,可以 \(O(nm)\) 的找出每個子串,然後排序,用冒泡去 \(O(n^2)\) 的求出逆序對個數。不過大概只有 zxy 這個 sb 會拿冒泡去求逆序對了(

對於 \(m \leq 5\) 的數據,排好序用 BIT 或者 mergesort 求一下就好了。

對於字符串隨機的數據,我們注意到在比較字典序的時候,第一個字符相同從而進入下一位比較的概率是 \(\frac{1}{25 \times 26}\),再進行一位比較的概率是上面這個概率的平方,類似的,我們發現每次期望比較的次數非常小,不會超過 \(5\),因此排序的時間複雜度就是 \(O(nT \log n)\),其中 \(T\) 是字符串隨機意義下期望的兩串比較次數。但是注意到把所有的字串都求出來會爆空間,所以直接在原串上掃就行了。不夠大概只有 zxy 這個 鐵憨憨 會把所有的子串都求出來叭(

對於全部的數據,我們考慮比較兩個字符串字典序的過程,從前往後掃兩個串的前綴,只要有某個長度使得兩個串在該長度對應字符不同,就可以通過這個字符來比較兩個串的字典序,因此我們考慮找到兩個字符串的第一個不同的前綴。而這個前綴的長度是可以二分的,即若找到了某個長度使得兩個串的該長度前綴長度不同,第一個不同的前綴的長度一定不大於這個長度,否則第一個不同的前綴長度一定大於這個長度。

而判斷兩個串的前綴是否相同可以用HASH來解決。這樣就可以做到時間複雜度 \(O(n \log n \log m)\) 了,其中 \(O(\log m)\) 是二分的複雜度。

至於怎麼求一個優秀的與字符順序有關但是與字符位置無關(因爲要求 \(ab\)\(ba\) 不同,要求與順序有關,但是如 \(abab\),要求判斷前兩個字符組成的字符串與後兩個字符組成的字符串相同,要求與字符位置無關)的 hash 函數,可以對原串 hash 一遍,然後對原串的差分 hash 一遍,然後對原串的二階差分 hash 一遍,一直 hash 下去就好了(

Code

(80 pts)

#include <cstdio>
#include <algorithm>

typedef long long int ll;

const int maxn = 50005;

int n, m, ans;
int v[maxn], w[maxn];
char S[maxn];

struct BIT {
  int A[maxn];

  inline int lowbit(const int x) { return x & -x; }

  inline void update(int x, const int v) { do A[x] += v; while ((x += lowbit(x)) <= n); }

  inline int query(int x) { int _ret = 0; do _ret += A[x]; while (x -= lowbit(x)); return _ret; }
};
BIT tree;

bool cmp(const int &a, const int &b);

int main() {
  freopen("sort.in", "r", stdin);
  freopen("sort.out", "w", stdout);
  scanf("%d %d\n%s", &n, &m, S + 1);
  for (int i = 1; i <= n; ++i) { v[i] = i; }
  std::sort(v + 1, v + 1 + n, cmp);
  for (int i = 1; i <= n; ++i) {
    w[v[i]] = i;
  }
  for (int i = 1; i <= n; ++i) {
    ans += tree.query(n) - tree.query(w[i]);
    tree.update(w[i], 1);
  }
  qw(ans, '\n', true);
  return 0;
}

inline bool cmp(const int &a, const int &b) {
  for (int len = 1, i = a, j = b; len <= m; ++i, ++j, ++len) {
    if ((i > n) || (j > n)) {
      return i > n;
    } else if (S[i] != S[j]) {
      return S[i] < S[j];
    }
  }
  return a < b;
}

(std)

#include <cstdio>

#define mo 1000000007
#define N 50055

int f[N],s[N],tmp[N],n,m,i,ch,ans;
long long hash[N],pow[N];

//二分+哈希求以i開頭的和以j開頭的兩個子串哪個字典序更小
bool lessThanOrEqual(int i, int j)
{
    if (i == j) return true;
    int l, r, k;
    long long hsi, hsj;
    //二分求i和j開始從左向右第一位不同的位
    l = 0;
    r = m+1;
    if (n-j+2 < r) r = n-j+2;
    if (n-i+2 < r) r = n-i+2;
    while (r-l > 1)
    {
        k = (l+r)/2;
        //子串[i,i+k-1]的哈希值
        hsi = hash[i+k-1]-hash[i-1]*pow[k]%mo;
        if (hsi < 0) hsi += mo;
        //子串[j,j+k-1]的哈希值
        hsj = hash[j+k-1]-hash[j-1]*pow[k]%mo;
        if (hsj < 0) hsj += mo;
        if (hsi == hsj) l = k; else r = k;
    }
    //s[i+l]和s[j+l]是第一位不同的位
    if (l == m) return true;
    return s[i+l] < s[j+l];
}

//歸併排序
void sort(int l, int r)
{
    if (l == r) return;
    int mi = (l+r)/2;
    sort(l, mi);
    sort(mi+1, r);
    int i=l, j=mi+1;
    int nt = l;
    while (i<=mi || j<=r)
    {
        bool ilej;
        if (i > mi) ilej = false;
        else
        if (j > r) ilej = true;
        else ilej = lessThanOrEqual(f[i],f[j]);
        if (ilej) tmp[nt++] = f[i++];
        else
        {
            tmp[nt++] = f[j++];
            //從右區間取數時,右區間和左區間之間產生了繼續對
            //累加答案
            ans += mi-i+1;
        }
    }
    for (i=l; i<=r; ++i) f[i] = tmp[i];
}

int main()
{
    freopen("sort.in", "r", stdin);
    freopen("sort.out", "w", stdout);
    scanf("%d%d", &n, &m);
    hash[0] = 0;
    pow[0] = 1;
    for (i=1; i<=n; ++i)
    {
        for (ch=getchar(); ch<=32; ch=getchar());
        s[i] = ch-96;
        //預處理hash[i]=子串[1,i]的哈希值
        hash[i] = (hash[i-1]*29+s[i])%mo;
        //預處理pow[i]=29^i
        pow[i] = pow[i-1]*29%mo;
        f[i] = i;
    }
    s[n+1] = 0;
    sort(1, n);
    printf("%d\n", ans);
    return 0;
}

C 巧克力

有一塊分成 \(n \times m\) 個格子的矩形巧克力,雖然形狀上很規整但質量分佈並不均勻,每一格有各自的重量 \(w_{i, j}\),用 \(n \times m\) 個正整數表示。你需要將這一整塊巧克力切成 \(k\) 小塊,要求每塊都是矩形,且它們的重量分別爲 \(a_1 \sim a_k\)。一塊巧克力的重量等於它包含的所有格子的重量之和。

切巧克力的時候,你可以每次選一塊大的巧克力,沿着某條格線橫向或縱向將其切成兩塊小的巧克力。切下來的小塊巧克力可以繼續切割。切割路線不能是折線或斜線。任何時候當前的所有巧克力塊都必須是矩形的。

對於給定的巧克力和分割要求,請你判斷是否存在一個切割方案滿足上述要求。

共有 \(T\) 組數據,時限 \(2s\)

Limitations

img

Solution

Algorithm \(1\)

判斷一下 a 加起來是否等於 \(m\),當 \(w\) 恆等於 \(1\) 且只有一行的時候,只要按照 \(a\) 去一個一個切即可。

可過測試點:\(1\)。期望得分 \(10~pts\)

Algorithm \(2\)

爆搜切幾刀從哪裏切,注意到每切一塊都會有一塊新的巧克力產生,因此最多切 \(k\) 刀,而對於每塊巧克力,都只有 \(O(n + m)\) 種切法,因此搜索樹的深度爲 \(k\),每個節點有 \((n + m)\) 個孩子。爆搜的複雜度爲 \(O(T~(n + m) ^ k)\)

可通過測試點:\(1,~2,~3,~4\),期望得分 \(40~pts\)

Algorithm \(3\)

zxy 那個 鐵憨憨 一樣讀錯題,以爲每切一刀都必須滿足一個 \(a\),然後寫個垃圾爆搜,也能得到 \(40 pts\)

Algorithm \(4\)

注意到 \(k\) 非常小,因此非常適宜狀壓。

\(f_{i, j, x, y, S}\) 爲左上角爲 \((i, j)\),右下角爲 \((x,~y)\) 的矩形,是否滿足 \(a\) 的狀態爲 \(S\) 的情況,轉移只要枚舉那一刀在哪裏切得滿足了哪些情況即可。寫成記搜非常好寫。

時間複雜度 \(O(T n^2 m^2 (n + m) 3^k)\)。空間複雜度 \(O(n^2m^2 \times 3^k)\)

可通過測試點:\(1~\sim 6\),期望得分 \(60~pts\)

Algorithm \(5\)

注意到複雜度的瓶頸在狀態數上,考慮優化狀態。

我們發現對於一個確定了左上角和右上角的矩形,如果再確定了它要滿足的 \(a\) 之和,那麼他的左下角和右下角就可以確定了。因此我們發現只要確定了 \(i,~j,~x\)\(S\),那麼 \(y\) 就可以被確定了,因此在搜索的時候將矩形和不等於 \(S\) 狀態下 \(a\) 之和的狀態剪掉,那麼搜到的狀態數就變成了 \(O(n^2 m \times 2^k)\)

於是這樣的時間複雜度 \(O(T \times n^2 \times m\times (n + m) \times 3^k)\)

可通過測試點:\(1~\sim 8\),期望得分 \(80~pts\)

Algorithm \(6\)

對於 \(w = 1\) 的點,我們注意到相當於拿一些小矩形拼成這樣一個大矩形。由於各個小矩形完全相同,我們不需要記錄具體該矩形是第幾行第幾列。因此可以設 \(f_{i, j, S}\) 是長爲 \(i\),寬爲 \(j\) 的矩形,能否拼出狀態爲 \(S\)\(a\),轉移時依然可以枚舉這一刀是怎麼切的。

時間複雜度 \(O(n \times m \times (n + m) \times 3^k)\)

可通過測試點:\(1,~3,~5,~7,~9\),期望得分 \(50~pts\)

Algorithm \(7\)

注意到在轉移的時候,我們已經枚舉了轉移到哪個集合,那麼我們就不再需要去枚舉從哪裏切這一刀,因爲a的和是確定的,豎向和橫向都最多隻有一種切刀的方法,具體在哪裏切這一刀,可以二分這個位置。這樣轉移的複雜度就被優化到了 \(O(\log m)\)。總時間複雜度 \(O(n^2 m 3^k \log m)\)。可以通過全部的測試點。

期望得分 \(100~pts\)

Code

(80分)

#include <cstdio>
#include <cstring>

typedef long long int ll;

const int maxn = 11;
const int maxt = 1030;

bool vis[maxn][maxn][maxn][maxn][maxt], frog[maxn][maxn][maxn][maxn][maxt];

int n, m, k, T;
int MU[maxn][maxn], A[maxn], sum[maxn][maxn], val[maxt];

void work();
void clear();
bool dfs(const int x, const int y, const int z, const int w, const int S);

int main() {
  freopen("chocolate.in", "r", stdin);
  freopen("chocolate.out", "w", stdout);
  qr(T);
  while (T--) {
    clear();
    work();
  }
  return 0;
}

void clear() {
  memset(A, 0, sizeof A);
  memset(MU, 0, sizeof MU);
  memset(val, 0, sizeof val);
  memset(vis, 0, sizeof vis);
  memset(sum, 0, sizeof sum);
  memset(frog, 0, sizeof frog);
  n = m = k = 0;
}

void work() {
  qr(n); qr(m); qr(k);
  for (int i = 1; i <= n; ++i) {
    for (int j = 1; j <= m; ++j) {
      qr(MU[i][j]);
      sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + MU[i][j];
    }
  }
  for (int i = 0; i < k; ++i) {
    qr(A[i]);
  }
  int ALL = (1 << k) - 1;
  for (int S = 1; S <= ALL; ++S) {
    for (int i = 0; i < k; ++i) if (S & (1 << i)) {
      val[S] += A[i];
    }
  }
  puts(dfs(1, 1, n, m, ALL) ? "yes" : "no");
}

bool dfs(const int x, const int y, const int z, const int w, const int S) {
  bool &thisv = vis[x][y][z][w][S], &thisf = frog[x][y][z][w][S];
  if (thisv) { return thisf; }
  thisv = true;
  if ((sum[z][w] - sum[x - 1][w] - sum[z][y - 1] + sum[x - 1][y - 1]) != val[S]) {
    return false;
  }
  if ((S & (S - 1)) == 0) {
    return thisf = true;
  }
  for (int i = x; i < z; ++i) {
    for (int S0 = S; S0; S0 = (S0 - 1) & S) if (dfs(x, y, i, w, S0) && dfs(i + 1, y, z, w, S ^ S0)) {
      return thisf = true;
    }
  }
  for (int i = y; i < w; ++i) {
    for (int S0 = S; S0; S0 = (S0 - 1) & S) if (dfs(x, y, z, i, S0) && dfs(x, i + 1, z, w, S ^ S0)) {
      return thisf = true;
    }
  }
  return false;
}

(std)

#include <cstdio>
#include <list>

#define MAXK 15
#define N 11

struct Quad
{
    int a, b, c, d;
    Quad(int _a, int _b, int _c, int _d): a(_a), b(_b), c(_c), d(_d) {}
};

std::list<Quad> lf, lfx, lfy;
char f[N][N][N][1<<MAXK],fx[N][N][N][1010],fy[N][N][N][1010];
int sumx[N][N][N],sumy[N][N][N],suma[1<<MAXK],bg[1<<MAXK],ed[1<<MAXK],c[15000000],e[MAXK+1],a[20],
    n,m,K,i,j,l,r,T,sta,nc,w;

/*
求:以j1爲左邊界、j2爲右邊界、i1爲上邊界的矩形中,下邊界爲多少的矩形
重量和是w。如果不存在則返回-1
用二分求
*/
int calcx(int j1, int j2, int i1, int w)
{
    // fx[j1][j2][i1][w]用於記錄該子問題有沒有被求結果
    // 已求結果則直接返回結果
    if (fx[j1][j2][i1][w] != 0) return fx[j1][j2][i1][w];
    // 未求結果,將該狀態加入待清空隊列
    lfx.push_back(Quad(j1,j2,i1,w));
    // 二分求i2的位置
    int l, r, k;
    l = i1-1;
    r = n+1;
    while (r-l > 1)
    {
        k = l+r>>1;
        if (sumx[j1][j2][k]-sumx[j1][j2][i1-1] <= w) l = k; else r = k;
    }
    if (sumx[j1][j2][l]-sumx[j1][j2][i1-1] != w) l = -1;
    return fx[j1][j2][i1][w]=l;
}

/*
求:以i1爲上邊界、i2爲下邊界、j1爲左邊界的矩形中,右邊界爲多少的矩形
重量和是w。如果不存在則返回-1
和上面對稱
*/
int calcy(int i1, int i2, int j1, int w)
{
    if (fy[i1][i2][j1][w] != 0) return fy[i1][i2][j1][w];
    lfy.push_back(Quad(i1,i2,j1,w));
    int l, r, k;
    l = j1-1;
    r = m+1;
    while (r-l > 1)
    {
        k = l+r>>1;
        if (sumy[i1][i2][k]-sumy[i1][i2][j1-1] <= w) l = k; else r = k;
    }
    if (sumy[i1][i2][l]-sumy[i1][i2][j1-1] != w) l = -1;
    return fy[i1][i2][j1][w]=l;
}

/*
求(i1,j1)~(i2,j2)的矩形能否切出sta中的巧克力
*/
bool work(int i1, int i2, int j1, int j2, int sta)
{
    //記憶化:求過了則直接返回
    if (f[i1][i2][j1][sta] != 0) return f[i1][i2][j1][sta]==1;
    if (bg[sta] == ed[sta]) return true;
    //未求過,將該狀態加入待清空隊列
    lf.push_back(Quad(i1,i2,j1,sta));
    int i, sta2, x, y;
    //枚舉sta的每個非空真子集
    for (i=bg[sta]; i<ed[sta]; ++i)
    {
        sta2 = c[i];
        
        //嘗試橫向切
        x = calcx(j1,j2,i1,suma[sta2]);
        if (x != -1)
        if (work(i1,x,j1,j2,sta2) && work(x+1,i2,j1,j2,sta-sta2))
        {
            f[i1][i2][j1][sta] = 1;
            return true;
        }
        
        //嘗試縱向切
        y = calcy(i1,i2,j1,suma[sta2]);
        if (y != -1)
        if (work(i1,i2,j1,y,sta2) && work(i1,i2,y+1,j2,sta-sta2))
        {
            f[i1][i2][j1][sta] = 1;
            return true;
        }
    }
    f[i1][i2][j1][sta] = -1;
    return false;
}

void dfs(int sta, int t)
{
    if (t == MAXK)
    {
        if (sta > 0) c[nc++] = sta;
        return;
    }
    if (sta&e[t]) dfs(sta-e[t], t+1);
    dfs(sta, t+1);
}

int main()
{
    freopen("chocolate.in", "r", stdin);
    freopen("chocolate.out", "w", stdout);
    e[0] = 1; 
    for (i=1; i<=MAXK; ++i) e[i] = e[i-1]*2;
    
    //預處理每個sta有哪些非空真子集,連續存儲在隊列c中
    nc = 1;
    for (sta=1; sta<e[MAXK]; ++sta)
    {
        bg[sta] = nc; //bg表示sta的子集在c中的開頭位置
        dfs(sta, 0); //dfs求sta的非空真子集
        --nc;
        ed[sta] = nc; //ed表示sta的子集在c中的結尾位置
    }
    
    scanf("%d", &T);
    while (T--)
    {
        scanf("%d%d%d", &n, &m, &K);
        for (i=1; i<=n; ++i)
        for (j=1; j<=m; ++j)
        {
            scanf("%d", &w);
            //sumy[i][j][k]:從第i行到第j行,從第1列到第k列構成的矩形的重量和
            sumy[i][i][j] = sumy[i][i][j-1]+w;
            //sumx[i][j][k]:從第i列到第j列,從第1行到第k行構成的矩形的重量和
            sumx[j][j][i] = sumx[j][j][i-1]+w;
        }
        for (l=1; l<n; ++l)
        for (r=l+1; r<=n; ++r)
        for (j=1; j<=m; ++j) sumy[l][r][j] = sumy[l][r-1][j]+sumy[r][r][j];
        for (l=1; l<m; ++l)
        for (r=l+1; r<=m; ++r)
        for (i=1; i<=n; ++i) sumx[l][r][i] = sumx[l][r-1][i]+sumx[r][r][i];
        
        for (i=1; i<=K; ++i) scanf("%d", &a[i]);
        //求出{ai}的各個子集的重量和
        //suma[sta]:sta中的巧克力的總重量
        for (sta=0; sta<e[K]; ++sta)
        {
            suma[sta] = 0;
            for (i=sta, j=1; i>0; i>>=1, ++j)
            if (i&1) suma[sta] += a[j];
        }
        
        // 如果所有ai的總重量!=巧克力的總重量
        if (suma[e[K]-1] != sumy[1][n][m])
        {
            printf("no\n");
            continue;
        }
        
        //lf、lfx、lfy用於記錄哪些狀態被記憶化了,用於之後清零
        lf.clear();
        lfx.clear();
        lfy.clear();
        
        if (work(1,n,1,m,e[K]-1)) printf("yes\n");
        else printf("no\n");
        
        //清零記憶化過的狀態
        for (std::list<Quad>::iterator it=lf.begin(); it!=lf.end(); ++it) f[it->a][it->b][it->c][it->d] = 0;
        for (std::list<Quad>::iterator it=lfx.begin(); it!=lfx.end(); ++it) fx[it->a][it->b][it->c][it->d] = 0;
        for (std::list<Quad>::iterator it=lfy.begin(); it!=lfy.end(); ++it) fy[it->a][it->b][it->c][it->d] = 0;
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章