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
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;
}