T1
Topcoder SRM 578 div1 Medium - WolfInZooDivOne
考慮轉化限制。在一個序列中填若干 \(1\),對每個 \(1\) 的上一個 \(1\) 的上一個 \(1\) 的位置有限制。設某個 \(1\) 的上一個 \(1\) 的位置為 \(pre_i\),則如果一個限制為 \([l, r]\),則要求 \(\forall i \in [l, r], pre_{pre_i} < l\)。這樣就可以 dp 了,狀態為 \(dp[i][j]\),表示前 \(i\) 位,要求最後一個位置是 \(1\),而最後一個位置的前一個 \(1\) 位置在 \(j\) 的方案數。然後根據限制隨便轉移一下就可以了。
程式碼
#include <iostream>
#define int long long
using namespace std;
const int P = 1000000007;
void input(int &n,int *fa){
int T,x;
string s,t;
scanf("%lld\n", &T);
for(;T--;){
getline(cin,t);
s+=t;
}
s+=' ',n=0,x=0;
for(char c:s){
if(c==' ')fa[++n]=x,x=0;
else if(isdigit(c))x=x*10+c-'0';
}
}
int n;
int l[305], r[305];
int dp[305][305];
int lim[305];
int m;
signed main() {
cin >> n;
input(m, l);
input(m, r);
for (int i = 1; i <= n; i++) lim[i] = 2147483647, dp[i][0] = 1;
for (int i = 1; i <= m; i++) {
++l[i], ++r[i];
for (int j = l[i]; j <= r[i]; j++)
lim[j] = min(lim[j], l[i]);
}
int ans = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
for (int k = 0; k < j; k++) {
if (k < lim[i])
dp[i][j] += dp[j][k];
}
dp[i][j] %= P;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++)
ans += dp[i][j];
}
cout << ans % P << "\n";
return 0;
}
T2
Topcoder SRM 569 div1 Medium - TheJediTest
直接貪心。從左往右考慮每一個位置,依次做一下操作:
- 如果左邊還沒滿(不是 \(K\) 的倍數),就儘量把當前位置的值往左扔,直到左邊滿了或者當前位置無人可走為止。如果左邊滿了,我們不去動它。
- 接下來嘗試把當前位置上多的東西往右扔。如果當前位的限制還足夠把這一位剩的全扔到右邊去,那就直接扔。否則就不扔了。
然後每次操作時維護一下每一位都有多少人以及每個位置上的限制就能過了。感性理解一下,發現確實是對的。
程式碼
#include <iostream>
#define int long long
using namespace std;
int a[25], b[25], c[25];
signed main() {
int n, k;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
cin >> k;
for (int i = 1; i <= n; i++) b[i] = a[i], c[i] = 0;
for (int i = 1; i <= n; i++) {
if (i != 1 && (b[i - 1] + c[i - 1]) % k) {
int tmp1 = b[i], tmp2 = k - (b[i - 1] + c[i - 1]) % k;
c[i - 1] += min(tmp1, tmp2), b[i] -= min(tmp1, tmp2);
}
if (i != n && b[i] >= (b[i] + c[i]) % k)
c[i + 1] += (b[i] + c[i]) % k, b[i] -= (b[i] + c[i]) % k;
}
int ans = 0;
for (int i = 1; i <= n; i++) ans += (b[i] + c[i] + k - 1) / k;
cout << ans << "\n";
return 0;
}
T3
Topcoder SRM 602 div1 Medium - PilingRectsDiv1
首先考慮如何最大化一個集合裡的長方形的交。首先所有矩形都應該堆在一個角上,這樣這堆矩形的交就是 \(\min\{x\} \times \min\{y\}\)。其次應該透過旋轉使得每個矩形兩個引數的大小關係一致,也就是所有矩形都滿足 \(a \le b\),否則交換 \(a, b\)。
證明
考慮兩個矩形 $(a, b)$ 和 $(c, d)$。不妨設 $a \le b, c \le d, a \le c$。否則就旋轉或交換兩個矩形。接下來可以發現所有本質不同的配對方式只有兩種:$a - c, b - d$ 和 $a - d, b - c$。於是接下來只要證明 $\min \{ a, c \} \times \min\{ b, d \} \ge \min \{ a, d \} \times \min\{ b, c \}$。於是只要證明 \(\min\{ b, d \} \ge \min\{ b, c \}\)。注意到 \(a \le c \le d\),所以來分討 \(b\) 與 \(c, d\) 的大小關係。
- \(b \le c \le d\),此時原式為 \(b \ge b\);
- \(c \le b \le d\),此時原式為 \(b \ge c\);
- \(c \le d \le b\),此時原式為 \(d \ge c\)。
容易發現這三個式子在各自的限制條件下都是成立的。故原式成立。
容易發現兩個矩形的交也是一個矩形,而且這個矩形兩維的大小關係和原來兩個矩形相同。於是多個矩形的情況就是拿兩個矩形交一下,然後把這個交拿去和另一個矩形交,這樣一直做下去。所以當每兩個矩形兩維大小關係都一樣時答案就是最優的。
交換完之後接下來就考慮進行分配。首先能夠發現最小的 \(x\) 永遠是其中一個集合中的最小 \(x\)。令為第一個集合。於是我們再列舉另一個集合裡的最小 \(x\) 是哪個矩形。為了方便,我們將所有矩形按 \(x\) 降序排,這樣列舉了另一個集合的最小 \(x\) 後就能知道這個集合裡剩下的元素都只能在這個元素之前。接下來還要分配 \(y\)。設當前列舉到 \(i\),則就是把前面最大的 \(n - 1\) 個 \(y\) 分給第二個集合,或者前面把最大的 \(i - n\) 個最大的 \(y\) 和後面所有 \(y\) 分配給第一個集合。這樣就要維護一個動態第 \(k\) 大。隨便拿個什麼東西亂搞就可以了。我這裡使用的是離散化 + 權值線段樹上二分。
程式碼
#include <iostream>
#include <algorithm>
#include <map>
#define int long long
using namespace std;
const int N = 1000000000;
const int inf = 2147483647;
pair<int, int> xy[1000005];
int n;
int XA, XB, XC, YA, YB, YC;
int x[1000005], y[1000005];
int XS[1000005], YS[1000005];
int suf[1000005];
int rt;
struct node {
int l, r, s;
} T[2000005];
struct Segment_Tree {
int ncnt;
void Insert(int& o, int l, int r, int v) {
if (!o)
o = ++ncnt;
T[o].s++;
if (l == r)
return;
int mid = (l + r) >> 1;
if (v <= mid)
Insert(T[o].l, l, mid, v);
else
Insert(T[o].r, mid + 1, r, v);
}
int Kth(int o, int l, int r, int k) {
if (!o)
return 0;
if (l == r)
return l;
int mid = (l + r) >> 1;
if (T[T[o].r].s >= k)
return Kth(T[o].r, mid + 1, r, k);
else
return Kth(T[o].l, l, mid, k - T[T[o].r].s);
}
} seg;
map<int, int> mp;
int _mp[1000005];
int d[1000005];
signed main() {
cin >> n;
int lengthofXS;
cin >> lengthofXS;
for (int i = 0; i < lengthofXS; i++) cin >> XS[i];
cin >> lengthofXS;
for (int i = 0; i < lengthofXS; i++) cin >> YS[i];
cin >> XA >> XB >> XC >> YA >> YB >> YC;
for (int i = 0; i < (lengthofXS); i++) {
x[i] = XS[i];
y[i] = YS[i];
}
for (int i = (lengthofXS); i < 2 * n; i++) {
x[i] = (x[i - 1] * XA + XB) % XC + 1;
y[i] = (y[i - 1] * YA + YB) % YC + 1;
}
for (int i = n * 2; i; i--) x[i] = x[i - 1], y[i] = y[i - 1];
for (int i = 1; i <= n * 2; i++) {
if (x[i] > y[i])
swap(x[i], y[i]);
xy[i] = make_pair(x[i], y[i]);
d[i] = y[i];
}
sort(d + 1, d + n * 2 + 1);
for (int i = 1; i <= n * 2; i++) _mp[mp[d[i]] = mp[d[i - 1]] + (d[i] != d[i - 1])] = d[i];
mp[0] = inf;
sort(xy + 1, xy + n * 2 + 1, greater<pair<int, int> >());
suf[n * 2 + 1] = inf;
for (int i = n * 2; i; i--) suf[i] = min(suf[i + 1], xy[i].second);
for (int i = 1; i < n; i++) seg.Insert(rt, 1, n * 2, mp[xy[i].second]);
int mn = inf;
int ans = 0;
for (int i = 1; i < n; i++) mn = min(mn, xy[i].second);
for (int i = n; i < n * 2; i++) {
if (i != n) {
int a = _mp[seg.Kth(rt, 1, n * 2, n - 1)], b = _mp[seg.Kth(rt, 1, n * 2, i - n)];
int c = xy[i].second, d = suf[i + 1];
ans = max(ans, max(xy[i].first * min(a, c) + min(mn, d) * xy[n * 2].first, xy[i].first * min(mn, c) + xy[n * 2].first * min(b, d)));
} else
ans = max(ans, xy[i].first * min(mn, xy[i].second) + suf[i + 1] * xy[n * 2].first);
seg.Insert(rt, 1, n * 2, mp[xy[i].second]);
mn = min(mn, xy[i].second);
}
cout << ans << "\n";
return 0;
}
T4
Topcoder SRM 582 div1 Medium - ColorfulBuilding
考慮 dp,從大到小加入每個建築,這樣如果要新加的建築被看到就必須加在最左邊。但是最左邊的建築顏色不確定,所以還要知道最左邊建築當前顏色。設 \(dp[i][j][k]\) 表示加入了前 \(i\) 高的建築,一共有 \(j\) 個建築能夠被看到,最左邊的建築顏色為 \(k\) 的方案數。設當前要轉移的狀態為 \(dp[i][j][k]\),要擺的建築顏色為 \(cur\)。首先如果當前建築不擺最左邊,則有 \(dp[i + 1][j][k] \leftarrow dp[i][j][k] * k\)。如果擺最左邊而 \(k \ne cur\),則 \(dp[i + 1][j + 1][cur] \leftarrow dp[i][j][k]\),否則 \(dp[i + 1][j][cur] \leftarrow dp[i][j][k]\)。這樣就有一個樸素的 \(n^3\) dp。考慮最佳化。發現對於一個 \(dp[i][j][k]\),它只有對 \(dp[i + 1][\cdots][cur]\) 的貢獻是特殊的。對於其他的都是乘上一個 \(i\)。所以可以使用懶標記的思想,對所有 \(cur\) 之外的顏色打一個標記,之後用的時候再乘上。接下來考慮對 \(dp[i + 1][j][cur]\) 的轉移。發現這個狀態首先受到 \(dp[i][j][cur] \times (i + 1)\) 的貢獻。其次還受到 \(dp[i][j][\forall k \ne cur]\) 的貢獻。所以我們對每種顏色維護懶標記,對每種 \(j\) 維護所有 \(k\) 的和 \(s\),每次轉移時先算出當前的 \(dp[i][\cdots][k]\),然後列舉 \(j\) 進行轉移,轉移時維護 \(s\) 即可。然後再給每個不為 \(cur\) 的顏色打上懶標記。這樣時間複雜度就是 \(\mathcal{O}(n^2)\) 的。
程式碼
#include <iostream>
#include <map>
#define int long long
using namespace std;
const int P = 1000000009;
int n;
string s1 = " ", s2 = " ", ss;
int clr[1305];
int dp[1305][1305];
map<int, int> mp;
int mul[1305];
int s[1305];
int ccnt;
signed main() {
cin >> n;
while (n--) {
cin >> ss;
s1 += ss;
}
cin >> n;
while (n--) {
cin >> ss;
s2 += ss;
}
n = s2.size() - 1;
for (int i = 1; i <= n; i++) {
int tmp = (s1[i] - 'a') * 10000 + s2[i] - 'a';
if (!mp.count(tmp))
mp[tmp] = ++ccnt;
clr[i] = mp[tmp];
}
for (int i = 1; i <= n / 2; i++) swap(clr[i], clr[n - i + 1]);
for (int i = 0; i < n; i++) clr[i] = clr[i + 1];
dp[1][clr[0]] = 1;
s[1] = 1;
for (int i = 1; i <= ccnt; i++) mul[i] = 1;
for (int i = 1; i < n; i++) {
int cur = clr[i];
for (int j = 1; j <= n; j++)
dp[j][cur] = (dp[j][cur] * mul[cur]) % P;
mul[cur] = 1;
for (int j = n; j; j--) {
s[j] = (s[j] * i) % P;
s[j] += dp[j][cur] + s[j - 1] - dp[j - 1][cur];
s[j] = (s[j] + P) % P;
dp[j][cur] = (dp[j][cur] * (i + 1)) % P;
dp[j][cur] += s[j - 1] - dp[j - 1][cur];
dp[j][cur] = (dp[j][cur] + P) % P;
}
for (int j = 1; j <= ccnt; j++) {
if (j != cur)
mul[j] = (mul[j] * i) % P;
}
// 三次方暴力
// for (int j = 1; j <= n; j++) {
// for (int k = 1; k <= ccnt; k++) {
// if (clr[i + 1] == k)
// dp[i + 1][j][k] = (dp[i + 1][j][k] + (i + 1) * dp[i][j][k]) % P;
// else {
// dp[i + 1][j][k] = i * dp[i][j][k] % P;
// dp[i + 1][j + 1][clr[i + 1]] = (dp[i + 1][j + 1][clr[i + 1]] + dp[i][j][k]) % P;
// }
// }
// }
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= ccnt; j++)
dp[i][j] = (dp[i][j] * mul[j]) % P;
}
int l;
cin >> l;
int ans = 0;
for (int i = 1; i <= ccnt; i++) ans += dp[l][i];
cout << ans % P << "\n";
return 0;
}
T5
Topcoder SRM 522 div1 Hard - PointErasing
首先觀察到 \(x, y\) 中至少有一維是極值時這個點就不會被刪掉。所以我們以縱座標為極值的最左邊點和最右邊點的橫座標為界限,將整個平面分成三個部分。首先中間部分的所有點都會被刪掉,一個不留。左邊部分中的點中,縱座標與 橫座標最小的點的縱座標(\(y_1\)) 相同的點不一定會被刪掉,剩下的點一定都會被刪掉。右半部分同理。接下來就是考慮這裡面到底有幾個點能留。先考慮左部分,則右邊部分是對稱的。接下來只考慮左半部分的點。發現只有縱座標在 \(y_1\) 之上的點和在 \(y_1\) 之下的點可以刪除縱座標為 \(y_1\) 的點。這些縱座標不為 \(y_1\) 的點構成了若干個矩形,每個矩形可以刪去一段區間的點。可以發現我們選的矩形一定不會有重疊,就算有重疊也可以透過調整使得選出不重疊的矩形達成相同的效果。所以直接考慮 dp。設 \(dp[i][j]\) 表示前 \(i\) 個點中留下 \(j\) 個點是否可行。轉移考慮是否刪去這個點。刪去就列舉所有以這個點為刪去的右端點的矩形進行轉移,不刪就從 \(dp[i - 1][j - 1]\) 裡轉移。
程式碼
#include <iostream>
#include <string.h>
#include <vector>
using namespace std;
int n;
int mn = 2147483647, mx, mnp1, mnp2, mxp1, mxp2;
int a[55];
int id[55], _id[55], ncnt;
vector<int> vec[55];
int b;
bool dp[55][55];
bool ok1[55], ok2[55], ok[55];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] > mx)
mx = a[i], mxp1 = mxp2 = i;
else if (a[i] == mx)
mxp2 = i;
if (a[i] < mn)
mn = a[i], mnp1 = mnp2 = i;
else if (a[i] == mn)
mnp2 = i;
}
int L = min(mnp1, mxp1), R = max(mnp2, mxp2);
for (int i = 1; i <= n; i++) b += (a[i] == mx || a[i] == mn);
for (int i = 1; i <= L; i++) {
if (a[i] == a[1] && i != L)
_id[id[i] = ++ncnt] = i;
if (a[i] > a[1] || i == L) {
for (int j = 1; j < i; j++) {
if (a[j] < a[1]) {
int lm = 0, rm = 0, cnt = 0;
for (int k = j + 1; k < i; k++) {
if (a[k] == a[1]) {
rm = k;
++cnt;
if (!lm)
lm = k;
}
}
vec[id[rm]].emplace_back(id[lm]);
}
}
}
if (a[i] < a[1] || i == L) {
for (int j = 1; j < i; j++) {
if (a[j] > a[1]) {
int lm = 0, rm = 0, cnt = 0;
for (int k = j + 1; k < i; k++) {
if (a[k] == a[1]) {
rm = k;
++cnt;
if (!lm)
lm = k;
}
}
vec[id[rm]].emplace_back(id[lm]);
}
}
}
}
dp[0][0] = 1;
for (int i = 1; i <= ncnt; i++) {
for (int j = 1; j <= ncnt; j++) {
dp[i][j] |= dp[i - 1][j - 1];
for (auto v : vec[i])
dp[i][j] |= dp[v - 1][j];
}
}
for (int i = 0; i <= ncnt; i++) ok1[i] = dp[ncnt][i];
for (int i = 1; i <= ncnt; i++) vec[i].clear();
ncnt = 0;
for (int i = n; i >= R; i--) {
if (a[i] == a[n] && i != R)
_id[id[i] = ++ncnt] = i;
if (a[i] > a[n] || i == R) {
for (int j = i + 1; j <= n; j++) {
if (a[j] < a[n]) {
int lm = 0, rm = 0, cnt = 0;
for (int k = i + 1; k < j; k++) {
if (a[k] == a[n]) {
rm = k;
++cnt;
if (!lm)
lm = k;
}
}
vec[id[lm]].emplace_back(id[rm]);
}
}
}
if (a[i] < a[n] || i == R) {
for (int j = i + 1; j <= n; j++) {
if (a[j] > a[n]) {
int lm = 0, rm = 0, cnt = 0;
for (int k = i + 1; k < j; k++) {
if (a[k] == a[n]) {
rm = k;
++cnt;
if (!lm)
lm = k;
}
}
vec[id[lm]].emplace_back(id[rm]);
}
}
}
}
memset(dp, 0, sizeof dp);
dp[0][0] = 1;
for (int i = 1; i <= ncnt; i++) {
for (int j = 1; j <= ncnt; j++) {
dp[i][j] |= dp[i - 1][j - 1];
for (auto v : vec[i])
dp[i][j] |= dp[v - 1][j];
}
}
for (int i = 0; i <= ncnt; i++) ok2[i] = dp[ncnt][i];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
if (ok1[i] && ok2[j])
ok[i + j + b] = 1;
}
}
for (int i = 1; i <= n; i++) {
if (ok[i])
cout << i << " ";
}
cout << "\n";
return 0;
}
腦洞要開啟。
dp 時可以選擇要知道什麼就把什麼扔到狀態裡。最佳化時可以只考慮特殊的轉移,平凡的轉移可以使用懶標記。
多觀察性質。多猜結論。