模拟赛好题分享
@
目錄-
山茶花
- 100pts
-
T1區間逆序對
- 60pts
- 100pts 區間操作固定套路,轉化為前綴操作
-
dream
- 20pts 神奇分塊
-
杭州:轉化題意,正難則反
- 正難則反(或者對于這種有刪邊操作的題), 我們看成反向加邊
- 看題:構造
- 坐飛機:斜率優化DP
- 抓頹 : 啟發式合并 + stl大雜燴
- 討厭的線段樹
-
Foo Fighters :構造
- 亂搞一:大膽隨機化
- 正解:位運算 -> 基本套路 按位考慮
- Hack it! : 構造
-
光之劍:計數DP
- 正解:正難則反,取補集
- Divide
-
正解:科技-Stern-Brocot樹 法里樹
-
Stern-Brocot樹
- 性質1 單調性
- 性質2 SB Tree的所產生的分數都是最簡分數
- 證明
- 法里樹
-
Stern-Brocot樹
-
醒幸:正難則反
- 相似題目:杭州
- 正解:轉化題意, 二分
-
ABC321F :退背包
- 正解:性質題
-
農場道路修建 : 問題轉化
- 正解 :問題轉化
- 方法一 :
- 方法二 :
-
密碼鎖:優化DP
- 25pts:DP
- 55pts:區間DP+線段樹優化
- 100pts:問題轉化+性質
-
魔法是變化之神
- 60pts:背包,求補集
- 100pts:考慮隨機數據
-
奶牛的數學題:單位貢獻
- 正解
- 路遇矩陣
- 奶牛的括號匹配:狀壓
-
潤不掉了:轉化點對問題
- 40pts
- 100pts 子樹貢獻轉化
-
美好的查詢:神奇主席樹
- 80pts:分塊+并查集
- 100pts:神奇主席樹,多看看
- 新涂色游戲:操作反做
- 新-滑動窗口簡單差分
- 小明去旅游 鍋
-
Heavy and Frail:分治優化重復操作
- 35pts,二進制分組背包暴力跑
- 80pts 背包合并
- 100pts
- chess:根據題意分析
- glass:簡單裝呀
- card:組合意義的DP
- godnumber:鍋
- meirin:暴力化簡式子
- sakuya:期望樹形DP好題
- 交換消消樂:簡單性質題
- [ABC232H] King's Tour:構造,鍋
- Road of the King:神奇DP
- 醉醉瘋瘋渺渺空空換根dp
- F - Robot Rotation 不能隨機化的折半搜索
- E - Revenge of "The Salary of AtCoder Inc." 期望
- A. 1031模擬賽-A進步科學 狀壓DP
- B. 1031模擬賽-B吉吉沒急 差分約束
- C. 1031模擬賽-C老杰克噠 動態DP
- 排座位 - 題目 - Daimayuan Online Judge] DP
- A. 方塊游戲 - 題目 - 多校信息學訓練題庫
- B. 雪球 - 題目 - 多校信息學訓練題庫
- D. 不要FQ - 題目 - 多校信息學訓練題庫 動態DP
- 23zr提高day9-美人魚 - 題目 - Zhengrui Online Judge (zhengruioi.com)
山茶花
性質推導題: 如果沒有+1操作, 那么最后的答案一定為恒定的ans
考慮+1操作對什么時候會產生影響
不難發現,如果后綴為k個1, 則+1操作等效于 $ans ^ (1 << (k + 1) )$
那么問題就轉化為了由這n任意順序異或,是否可以異或出后綴為k個1的數
如果第0位的1都沒有拼湊成功,那么后綴1再長也沒有意義,所以如果要想異或出k個1,必須要保證前$k - 1$個1異或出來且不能因為第k位1的異或影響
思考線性基異或最大值時,保證了優先異或出高位1且高位1不會被低位1影響
所以可以構造低位線性基進行判斷 復雜度$n * log_{2}n$
100pts
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 2e6 + 70;
int n;
LL a[MAX], b[MAX], SUM = 0, ans = 0;
void add(LL X) {
for(int i = 0; i <= 61; i++) if(X >> i & 1) {
if(b[i]) X ^= b[i];
else {
b[i] = X;
// printf("b[%d] %lld\n", i, b[i]);
return ;
}
}
}
bool check(int X) {
LL NOW = 0;
for(int i = 0; i <= X; i++) {
// printf("X %d NOW %lld\n", X, NOW);
if((NOW >> i & 1) != (i != X)) {
if(b[i]) NOW ^= b[i];
else return 0;
}
}
return 1;
}
int main() {
// freopen("shuju.in","r",stdin);
// freopen("mine.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
SUM ^= a[i];
add(a[i]);
}
// cout<<SUM<<endl;
for(int i = 0; i <= 61; i++) { // 使末尾有i個1
if(check(i)) {
ans = max(ans, SUM ^ (1ll << (i + 1)) - 1);
// printf("i %d %lld\n",i, ans);
}
}
cout<<ans<<endl;
return 0;
}
T1區間逆序對
60pts
1e5莫隊即可,主要復習一下回滾莫隊
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 1e6 + 70;
int n, m, a[MAX], bl[MAX], blen;
int t1[52], t2[52], k[52];
LL ANS[MAX];
struct made {
int l, r, id;
}ask[MAX];
bool mycmp(made X, made Y) { return (bl[X.l] < bl[Y.l]) || (bl[X.l] == bl[Y.l] && X.r < Y.r); }
int lowbit(int x) { return (x & (-x)); }
void add1(int x) { for(int i = x; i; i -= lowbit(i)) t1[i] += 1; }
LL Find1(int x) { LL res = 0; for(int i = x; i <= 50; i += lowbit(i)) res += t1[i]; return res;}
void add2(int x) { for(int i = x; i; i -= lowbit(i)) t2[i] += 1; }
LL Find2(int x) { LL res = 0; for(int i = x; i <= 50; i += lowbit(i)) res += t2[i]; return res;}
int main() {
scanf("%d%d", &n, &m);
blen = pow(n, 2.0 / 3.0);
// printf("blen %d\n", blen);
for(int i = 1; i <= n; i++) bl[i] = (i / blen) + 1;
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= m; i++) {
scanf("%d%d",&ask[i].l, &ask[i].r);
ask[i].id = i;
}
sort(ask + 1, ask + 1 + m, mycmp);
int LST = 0, L = 0, R = 0;
LL ans = 0;
for(int i = 1; i <= m; i++) {
if(bl[ask[i].l] == bl[ask[i].r]) { //同一塊中,暴力求解
memset(t1, 0, sizeof(t1)); LL res = 0;
for(int j = ask[i].l; j <= ask[i].r; j++) {
res += Find1(a[j] + 1);
add1(a[j]);
}
ANS[ask[i].id] = res;
} else {
// printf("ask[%d].id %d l %d r %d\n", i, ask[i].id, ask[i].l, ask[i].r);
if(bl[ask[i].l] != LST) { //不在一個塊中
memset(t2, 0, sizeof(t2));
L = (blen * bl[ask[i].l]);
R = L - 1;
ans = 0;
LST = bl[ask[i].l];
}
// printf("L %d R %d\n", L, R);
while(R < ask[i].r) {
ans += Find2(a[++R] + 1);
add2(a[R]);
}
LL res = ans; //儲存
for(int j = 1; j <= 50; j++) k[j] = t2[j];
while(L > ask[i].l) {
L--;
ans += (R - L - Find2(a[L]));
add2(a[L]);
}
ANS[ask[i].id] = ans;
ans = res;
L = (blen * bl[ask[i].l]);
for(int j = 1; j <= 50; j++) t2[j] = k[j]; //撤銷
}
}
for(int i = 1; i <= m; i++) printf("%lld\n", ANS[i]);
return 0;
}
100pts 區間操作固定套路,轉化為前綴操作
觀察數據范圍, a的數值都小于等于50, 而每次查詢都是一個區間,面對區間問題,最常用的套路就是預處理出前綴數組,O(常數)查詢
思考前綴逆序對F性質,對于區間L,R而言, 前綴F[R] 包含了 $L < l < r < R,l < r < L, l < L < r < R$ 三種逆序對
對于前兩種逆序對,F[R] - F[L] 即可, 主要需要撤銷l不在[L,R]中, r在[L, R]中的逆序對, 這里就需要利用$a <= 50$, 看[L, R]中每種1-50出現了多少次即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const LL MAX = 1e6 + 70;
int tree_g[MAX];
int tree_n[MAX];
int a[MAX];
LL n, m;
int sum[MAX][52];
LL NINI[MAX];
int num[MAX][52];
int lowbit(int x) {
return (x & (-x));
}
int Find_g(int x) {
int res = 0;
for(int i = x; i <= 50; i += lowbit(i)) res += tree_g[i];
return res;
}
void ADD_g(int x) {
for(int i = x; i; i -= lowbit(i)) tree_g[i] += 1;
}
int Find_n(int x) {
int res = 0;
for(int i = x; i <= 50; i += lowbit(i)) res += tree_n[i];
return res;
}
void ADD_n(int x) {
for(int i = x; i; i -= lowbit(i)) tree_n[i] += 1;
}
signed main() {
// freopen("ex_data3.in","r",stdin);
// freopen("mine.out","w",stdout);
scanf("%lld%lld", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= 50; j++) {
sum[i][j] = sum[i - 1][j];
}
sum[i][a[i]] += 1;
}
for(int i = 1; i <= n; i++) { //處理前綴中比j大的個數
ADD_g(a[i]);
for(int j = 1; j <= 50; j++) {
num[i][j] = Find_g(j + 1);
}
}
for(int i = 1; i <= n; i++) {
NINI[i] = NINI[i - 1];
LL res = Find_n(a[i] + 1);
NINI[i] += res;
ADD_n(a[i]);
}
// printf("%lld\n", num[2][2]);
for(int i = 1; i <= m; i++) {
int l, r; scanf("%d%d", &l, &r);
LL ans = NINI[r] - NINI[l - 1];
for(int j = 1; j <= 50; j++) {
ans = ans - ((LL)num[l - 1][j] * (LL)(sum[r][j] - sum[l - 1][j]));
}
printf("%lld\n", ans);
}
return 0;
}
dream
20pts 神奇分塊
對于$n * m <= 5e7$的數據考慮分塊做法,發現如果沒有回歸操作,那么僅需要樹狀數組即可維護,但是我們發現對于序列(l, r)而言, 盡管它們的標記點不同,但是通過一次回歸將所有標記清空后對于它們整體的操作所帶來的偏移量一樣!那么就可以分塊解決了
沒調過的代碼
杭州:轉化題意,正難則反
數據范圍 $n,m <= 200000$,首先很明顯的事情,在一顆樹中距離$x$最遠的點一定是該直徑的其中一個端點
那么題目要求我們求得就是在原樹中任一子樹的直徑端點分別是什么,但是我們無法維護任一形態的子樹
正難則反(或者對于這種有刪邊操作的題), 我們看成反向加邊
那么現在我們要求的就是合并兩個聯通塊后直徑端點是什么
假設兩個聯通塊分別為S1,S2, 直徑端點分別$S1.L, S1.R S2.L, S2.R$, 連通塊交點為X, 在兩個連通塊中距離X的最遠的端點都在直徑端點上
X將兩個連通塊聯通,若直徑沒變,則應該為S1,S2中直徑更大值,若發生改變,則一定經過X,所以發生更改的最遠直徑的端點一定為$S1.L,S1.R,S2.L,S2.R$
現在我們可以維護出連通塊的最長直徑和直徑端點,因為這兩個聯通塊合并后再在原樹(假定1為根)中形態不變
所以用(DEP[X] + DEP[Y] - DEP[LCA(X,Y)])維護即可
//查詢距離x最遠的點,一定為樹上直徑端點之一, 刪邊樹的直徑不好維護
//考慮轉化為加邊維護聯通塊中的樹的直徑, 那么需要對于每次加邊操作就重新跑一遍lca嗎?
//顯然不需要, 如果我們處理出整顆樹,那么我們只需要確定那兩個點是樹的直徑, 在原樹中確定即可
//不關心樹的形態,只關心直徑的端點
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 2e5 + 70;
int n, m, cut[MAX], dep[MAX], tot, head[MAX];
int father[MAX], ANS[MAX], fa[MAX][22];
struct NODE {int x, y, dis;} p[MAX]; //分別表示這個點所處的聯通塊的直徑端點和直徑長度
struct made { int l, t, id;}edge[MAX * 2], E[MAX];
struct ASK{ int op, x; }ask[MAX];
void add(int u, int v, int id) {
edge[++tot].l = head[u];
edge[tot].t = v;
edge[tot].id = id;
head[u] = tot;
}
void dfs(int x, int FA, int DEP) { //預處理整顆樹的fa
fa[x][0] = FA; dep[x] = DEP;
for(int i = 1; i <= 20; i++) fa[x][i] = fa[fa[x][i - 1]][i - 1];
for(int i = head[x]; i; i = edge[i].l) {
int t = edge[i].t;
if(t == FA) continue;
dfs(t, x, DEP + 1);
}
}
int Find(int x) { return father[x] == x ? x : Find(father[x]); }
LL LCA(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
for(int i = 20; i >= 0; i--) if(dep[fa[x][i]] >= dep[y]) x = fa[x][i]; //往上跳
if(x == y) return y;
for(int i = 20; i >= 0; i--) if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[y][0];
}
int check(int x, int y) { return (dep[x] + dep[y]) - 2 * dep[LCA(x, y)]; }
NODE mrg(NODE X, NODE Y) {
NODE NOW = (X.dis > Y.dis) ? X : Y;
// printf("NOW %d X.x %d X.y %d Y.x %d Y.y %d \n",NOW.dis, X.x, X.y, Y.x, Y.y);
if(check(X.x, Y.x) > NOW.dis) {
NOW = {(NODE){X.x, Y.x, check(X.x, Y.x)}};
// printf("FIRST NOW.x %d NOW.y %d NOW.dis %d\n", NOW.x, NOW.y, NOW.dis);
}
if(check(X.x, Y.y) > NOW.dis) {
NOW = {(NODE){X.x, Y.y, check(X.x, Y.y)}};
// printf("SECOND NOW.x %d NOW.y %d NOW.dis %d\n", NOW.x, NOW.y, NOW.dis);
}
if(check(X.y, Y.x) > NOW.dis) {
NOW = {(NODE){X.y, Y.x, check(X.y, Y.x)}};
// printf("THIRD NOW.x %d NOW.y %d NOW.dis %d\n", NOW.x, NOW.y, NOW.dis);
}
if(check(X.y, Y.y) > NOW.dis) {
// printf("FORTH NOW.x %d NOW.y %d NOW.dis %d\n", NOW.x, NOW.y, NOW.dis);
NOW = {(NODE){X.y, Y.y, check(X.y, Y.y)}};
}
return NOW;
}
void merge(int x, int y) {
int fx = Find(x), fy = Find(y);
father[fx] = fy;
// printf("fx %d fy %d\n", fx, fy);
p[fy] = mrg(p[fx], p[fy]); //直徑
}
void work() {
dfs(1, 0, 0);
for(int i = 1; i <= n; i++) father[i] = i, p[i] = {i, i, 0};
for(int i = 1; i < n; i++) {
if(cut[i] == 0) {
// printf("E[%d] u %d v %d\n", i, E[i].l, E[i].t);
merge(E[i].l, E[i].t); //合并兩個聯通塊
}
}
for(int i = 1; i <= n; i++) {
int fx = Find(i);
// printf("i %d fa %d l %d r %d\n", i, fx, p[fx].x, p[fx].y);
}
for(int i = m; i >= 1; i--) {
if(ask[i].op == 1) merge(E[ask[i].x].l, E[ask[i].x].t);
else {
int fx = Find(ask[i].x);
ANS[i] = max(check(ask[i].x, p[fx].x), check(ask[i].x, p[fx].y));
}
}
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
add(u, v, i); add(v, u, i);
E[i] = {(made){u, v, i}};
}
for(int i = 1; i <= m; i++) {
scanf("%d%d", &ask[i].op, &ask[i].x);
if(ask[i].op == 1) cut[ask[i].x] = 1;
}
memset(ANS, -1, sizeof(ANS));
work(); //
for(int i = 1; i <= m; i++) {
if(ANS[i] != -1) printf("%d\n", ANS[i]);
}
return 0;
}
看題:構造
坐飛機:斜率優化DP
抓頹 : 啟發式合并 + stl大雜燴
討厭的線段樹
Foo Fighters :構造
亂搞一:大膽隨機化
只需要輸出一個S即可,直接隨機化,如果不捆綁的話應該可以取得不少的分數
正解:位運算 -> 基本套路 按位考慮
&運算有一個性質 只有(1 & 1) =1 所以最高有效位數低的數字的1的個數的奇偶性不會被較高位1的&運算影響(更自然)
所以我們如果按照位數低向高進行枚舉這樣可以保證對較高位的運算不會對較低位的答案產生影響
這時候我們發現一個問題,什么是較高位的答案,什么是較低位的答案,明確下個定義:最高位1位置的大小
這樣我們將數進行分組,按照最高位進行分組,從低到高枚舉,最高位低組的答案一定不會被最高位高組影響
但是僅有這個性質是不夠的,會不會較低位的&運算會影響較高位的答案?
但是讓我們回想每組的答案跟什么有關系
只跟奇偶性有關系,所以最高位的一個1就可以將這一組分成兩組奇數組與偶數組,通過改變最高位1,就可以交換奇數組和偶數組
Hack it! : 構造
光之劍:計數DP
正解:正難則反,取補集
我們從數據入手,一步一步分析,如果$n,k <= 10$的話顯然直接搜索即可
但是第2檔分與爆搜差距過大,所以這檔分顯然需要我們給出一個$n^2$的做法
肯定可以想到這應該是一個計數類的DP,如何設計狀態
如果我們正向去做設$f[i]$表示以$i$為錯誤答案的貢獻
我們發現有兩個限制
$1.保證不是n的數能夠有后k個比它小的數$
$2.保證它前面沒有出現比他大的數 并且沒有出現錯誤答案$
如果嘗試過發現$f[i]$ 與 $f[j]$的聯系過小, 細節非常難處理
這時候我們要不斷調整狀態的設計,將一個大問題轉成一個子問題
這時候我們反向思考,如果知道了返回最大值為$n$的數量$res$,$(n! - res)$即為答案
那么什么時候會出現返回值為n?
兩種情況
1.n個數沒有出現返回值
2.讓n后面有j個數,且前面的數沒有返回值
這時候我們發現第二種情況需要用到第一種情況,那么我們思考如何解決第一種即可
設$f[x]$表示$x$個數且沒有返回值,$f[j] = j!(j∈[0,k])$,如何轉移?
這時候其實就非常簡單了,如果我們將第$i$個數放在$j$的位置上,我們需要保證前$j - 1$個數沒有出現返回值,且$i$后面不能有$k$個數
$f[i] = f[j-1] * C_{i - 1}^{j-1} * A_{i -j}^{i -j}(j∈[i - k+1,i])$
這樣就能拿到$60pts$考慮化簡發現隨著$i$的增加$1$,$j$的左邊界少1,右邊界大1,將組合數化簡$f[i] = f[j - 1] * (i-1)!/(j-1)!$這樣雙指針即可!
/*
發現正著做非常困難, 正難則反
考慮如果n個數的排列最大值能夠選到n的情況那么分為兩種, 一種是可以選到的, 一種是沒有選出來來的
如果n在前k個則一定能取到, 如果在k + 1個之后
需要保證前k個沒有產生最大值 (即沒有選出來的)
問題轉化為了子問題, 前i個數選不出來
*/
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 1e6 + 70;
const int MOD = 1e9 + 7;
int n, k;
LL A[MAX], f[MAX], INV[MAX];
LL quick_mi(LL x, LL y) {
LL xx = x, res = 1;
while(y) {
if(y % 2) res = res * xx % MOD;
xx = xx * xx % MOD;
y /= 2;
}
return res;
}
LL C(int x, int y) { return (A[x] * INV[y] % MOD * INV[x - y] % MOD); }
void prework() {
A[0] = 1;
for(int i = 1; i <= n; i++) A[i] = (A[i - 1] * i) % MOD;
for(int i = 0; i <= k; i++) f[i] = A[i]; //隨便放
for(int i = 0; i <= n; i++) INV[i] = quick_mi(A[i], MOD - 2); //預處理, 少log!!!!!
LL ANS = 0;
for(int i = k + 1; i >= 2; i--) ANS = (ANS + (f[i - 1] * A[k] % MOD * INV[i - 1] % MOD) ) % MOD;
f[k + 1] = ANS;
int l = 1;
for(int i = k + 2; i <= n; i++) { //前i個位置
ANS = (ANS - (f[l] * A[i - 1 - 1] % MOD * INV[l] % MOD) + MOD) % MOD;
ANS = ANS * (i - 1) % MOD;
ANS = ANS + (f[i - 1] * A[i - 1] % MOD * INV[i - 1] % MOD);
f[i] = ANS;
l++;
// for(int j = i; j >= i - k + 1; j--) { //i放的位置
// f[i] = (f[i] + (f[j - 1] * C(i - 1, j - 1) % MOD * A[i - j] % MOD) )% MOD; //可以化簡 ******
//// printf("j %d f[%d] %lld\n",j, i, f[i]);
// }
// printf("f[%d] %lld\n", i, f[i]);
}
}
int main() {
freopen("arisu.in","r",stdin);
freopen("arisu.out","w",stdout);
scanf("%d%d", &n, &k);
prework(); //預處理前i個數選不出來的數量
LL res = f[n];
// cout<<f[n]<<endl;
for(int i = 1; i <= n - k; i++) {
res = (res + (f[i - 1] * C(n - 1, i - 1) % MOD * A[n - i]) )% MOD;
}
cout<<(A[n] - res + MOD) % MOD<<endl;
return 0;
}
Divide
正解:科技-Stern-Brocot樹 法里樹
Stern-Brocot樹
我們定義$la = 0,lb=1,ra=1,rb=0$,
令$x=la+ra,y=lb+rb$,顯然得到$\cfrac{la}{lb}<\cfrac{x}{y}<\cfrac{ra}{rb}$將得到的$\cfrac{x}{y}$重新作為$la, lb, ra,rb$ 重新進行計算,即可得到上圖
性質1 單調性
性質2 SB Tree的所產生的分數都是最簡分數
證明
法里樹
將右邊界改為$\cfrac{1}{1}$,所得到的都為<=1的分數
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define LB long double
const int MAX = 1e7 + 7;
long double eps = 1e-19;
int fa, fb;
bool flag = 0;
long double val;
int main() {
// scanf("%d%d", &fa, &fb);
fa = 1000000000, fb = 1000000000;
scanf("%Lf", &val);
int la = 0, lb = 1, ra = 1, rb = 0;
long double lc = val;
int ansa = 1, ansb = 0;
while(1) {
int x = la + ra, y = lb + rb;
if(x > fa || y > fb) break;
LB NOW = ((LB)x / (LB)y);
if(fabs(NOW - val) < eps) {
ansa = x, ansb = y;
break;
}
else {
ansa = x, ansb = y;
lc = fabs(NOW - val);
if(NOW - val < 0) {
la = x, lb = y;
} else {
ra = x, rb = y;
}
}
}
printf("%d %d", ansa, ansb);
return 0;
}
醒幸:正難則反
相似題目:杭州
簡概題意:給定一個圖,每次刪去邊權和最大的森林(即刪去的圖必須聯通且沒有環), 求每條邊是哪次操作刪去的
正解:轉化題意, 二分
我們發現每次刪去一個最大的森林非常難處理,因為M的數據范圍是$M<=3e5$需要一個$log_{m} || \sqrt{m}$復雜度的算法,非常難維護
觀察數據范圍$K*N<=1e7$也就是說,我們如果可以將刪去$K$次與$N$產生聯系即可,刪去的圖聯通沒有環,顯然是一個樹形結構,那么也就是說我們要把$M$條邊進行分組,生成$K$棵樹,每一顆樹對應了一個刪除順序
現在問題就進行了轉換,如何向K個森林里加邊,保證沒有環且K個森林的邊權和從大到小?
對于K個森林邊權和從大到小,類Kruskal, 邊權從大到小,依次判斷往哪個森林加邊,如果當前森林中兩點不聯通,加邊,否則,向后判斷
到這里我們發現時間復雜度仍然劣,$MKlog_{n}$,瓶頸出在哪里?顯然是判斷往哪個森林里加邊,我們顯然想要一個$log$做法,考慮二分,這樣思考,如果對于當前這個森林已經聯通,顯然會在后邊的森林加邊,如果不聯通,顯然會在前面的森林中,感性理解,這樣問題就得以解決
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 2100 + 70;
int n, m, k, ans[MAX * MAX];
struct made {
int fa[1100];
LL val;
int Find(int x) {
if(fa[x] == x) return x;
return fa[x] = Find(fa[x]);
}
}P[10002];
struct node { int u, v, val, id; } e[MAX * MAX];
bool mycmp(node X, node Y) { return X.val > Y.val; }
bool mycmp2(node X, node Y) { return X.id < Y.id; }
bool check(int u, int v, int x) {
if(P[x].Find(u) == P[x].Find(v)) return 0;
return 1;
}
int add(int x) {
int l = 1, r = k, res = 0; //查詢k個聯通塊
while(l <= r) {
int mid = (l + r) >> 1;
if(check(e[x].u, e[x].v, mid)) {
res = mid;
r = mid - 1;
} else l = mid + 1;
}
if(res == 0) return 0;
int fu = P[res].Find(e[x].u);
int fv = P[res].Find(e[x].v);
P[res].fa[fu] = fv;
return res;
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= m; i++) {
scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].val);
e[i].id = i;
}
for(int i = 1; i <= k; i++)
for(int j = 1; j <= n; j++) P[i].fa[j] = j; //初始化
sort(e + 1, e + 1 + m, mycmp);
for(int i = 1; i <= m; i++) {
int now = add(i); //將第i個邊加入連通塊
ans[e[i].id] = now;
}
for(int i = 1; i <= m; i++) printf("%d\n", ans[i]);
return 0;
}
ABC321F :退背包
正解:性質題
如果只有$+$我們發現就是一道背包題,但是如果有$-$操作呢?
但是$+$操作的順序對答案有影響嗎?顯然是沒有的,題目保證不會出現刪去沒有出現過的數,我們不妨讓刪去的數放在整個序列的最后一個,倒著刪一下即可
//若只有+則序列順序無所謂 直接背包
//若有- 則考慮將序列構造成 -的數放在最后一個
//反向-一下即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 5100;
const int MOD = 998244353;
int q, k, f[MAX + 10];
int main() {
scanf("%d%d", &q, &k);
f[0] = 1;
while(q, q--) {
char ch; int x;
scanf("\n%c%d", &ch, &x);
if(ch == '+') {
for(int i = MAX; i >= x; i--) {
f[i] = (f[i - x] + f[i] ) % MOD;
}
} else {
for(int i = x; i <= MAX; i++) {
f[i] = (f[i] - f[i - x] + MOD) % MOD;
}
}
printf("%d\n", f[k]);
}
return 0;
}
農場道路修建 : 問題轉化
正解 :問題轉化
題目的大致含義為在$\frac{N * (N - 1)}{2}$的點對中增加一條邊后的基環樹的最大點支配集與原樹的大小保持不變
如何思考:
1.我們首先發現在樹上增加一條新的邊一定不會讓最大點支配集的個數變大, 其影響一定只會變小和保持不變
2. 如果在某一種最大點支配集的選擇方案中,
(選擇點, 選擇點) 會讓方案變小,
(無, 選擇點),(無, 無)不會讓答案變小
顯然性質2很好實現,但如果一顆樹有多種最大點支配集選擇方案該怎么處理, 這時候會出現許多重復
方法一 :
用總數減去(必選點,必選點) 的方案
方法二 :
(無)點: 在某種最大點支配集的選擇方案中可以不選的點
用(無)點統計答案, 因為(無)點與其他任意一個點連都可以,但(無)點之間會多連,所以答案為$Wu_{sum} (n - 1) -\frac{Wu_{sum}(Wu_{sum}-1)}{2}$
選擇方法2: 具體方法已經清楚,如何將多種最大點支配集的(無)點選出來?
首先對原樹做一遍最大點支配集,從根向下遍歷
如果$bol ==1$即這個點選擇,向下遍歷時選擇$son[x]$,$bol$修改為0
如果$bol == 0$即這個點不選,向下遍歷時$bol$修改為$f[][0/1]$較大的,如果一樣同時遍歷
如果當前點的$bol==0$打上標記,最后統計總數
方法2
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 3e5 + 70;
int n, tot, head[MAX];
bool flag[MAX];
LL f[MAX][3], ans; // 0 / 1 不選, 選
int mp[MAX][2];
vector<int> son[MAX];
struct made {
int l, t;
}edge[MAX * 2];
void add(int u, int v) {
edge[++tot].l = head[u];
edge[tot].t = v;
head[u] = tot;
}
void dfs_pre(int x, int fa) {
// printf("x %d fa %d\n", x, fa);
for(int i = head[x]; i; i = edge[i].l) {
int t = edge[i].t;
if(t == fa) continue;
son[x].push_back(t);
dfs_pre(t, x);
}
}
void dfs(int x) {
if(son[x].size() == 0) {
f[x][1] = 1; f[x][0] = 0;
return ;
}
int len = son[x].size();
for(int i = 0; i < len; i++) {
dfs(son[x][i]);
f[x][1] += f[son[x][i]][0];
f[x][0] += max(f[son[x][i]][0], f[son[x][i]][1]);
}
f[x][1] += 1;
return ;
}
void dfs_work(int now, int bol) {
if(mp[now][bol]) return ;
mp[now][bol] = 1;
if(bol == 0) flag[now] = 1;
for(int i = 0; i < son[now].size(); i++) {
int to = son[now][i];
if(bol == 0) {
if(f[to][0] > f[to][1]) dfs_work(to, 0);
else if(f[to][0] == f[to][1]) {
dfs_work(to, 0);
dfs_work(to, 1);
}
else dfs_work(to, 1);
} else {
dfs_work(to, 0);
}
}
}
int main() {
freopen("road.in","r",stdin);
freopen("road.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
add(u, v); add(v, u);
}
dfs_pre(1, 0);
dfs(1);
if(f[1][0] > f[1][1]) {
dfs_work(1, 0);
}
else if(f[1][0] == f[1][1]) {
// cout<<"ooo";
dfs_work(1, 0);
dfs_work(1, 1);
}
else dfs_work(1, 1);
LL sum1 = 0, sum2 = 0;
for(int i = 1; i <= n; i++) {
if(flag[i] == 1) sum1++, ans += (n - 1);
else sum2++;
}
// cout<<ans<<" "<<sum1<<endl;
cout<<ans - ((sum1 - 1) * sum1 / 2)<<endl;
return 0;
}
密碼鎖:優化DP
25pts:DP
首先排除貪心的思路, 對于前三擋我們希望擁有一個帶常數的$N*Q$的算法,考慮設計DP狀態,$f[i][j]$表示填充前$i$位,第$i$為$j$的最小代價
時間復雜度$NQ26*26$ 勉強跑過?
55pts:區間DP+線段樹優化
發現上面的狀態沒有拓展性,希望轉化狀態,然后發現修改先后順序無所為,只需要讓兩個相鄰區間合并時滿足$Lr <= Rl$即可,考慮區間DP$f[L][R][l][r]$表示左右端點為$L,R$左右填充$l, r$的最小代價
考慮簡化狀態:我們發現設定的DP狀態中很多都不必要存在,修改符合結合率,考慮線段樹倍增優化這個狀態
#include<bits/stdc++.h> // 25 感覺可以優化(DP 區間可合并, 線段樹)
using namespace std;
#define LL long long
const int MAX = 1e5 + 70;
int fp[MAX][30];
int q, len;
char ch[MAX];
int cha(int x, int y) { //x -> y的最小花費
if(x < y) return y - x;
else return x - y;
}
struct SegmentTree {
int l, r;
int f[6][6]; //左 端點, 右端點分別填啥
#define l(x) tree[x].l
#define r(x) tree[x].r
#define t(x) tree[x]
}tree[MAX * 4];
void update(int p) {
memset(t(p).f, 0x3f, sizeof(t(p).f));
for(int ll = 1; ll <= 5; ll++) {
for(int lr = ll; lr <= 5; lr++) {
for(int rl = lr; rl <= 5; rl++) {
for(int rr = rl; rr <= 5; rr++) {
t(p).f[ll][rr] = min(t(p).f[ll][rr], t(2 * p).f[ll][lr] + t(2 * p + 1).f[rl][rr]);
// printf("p %d l %d r %d f[%d][%d] %d\n",p, l(p), r(p), ll, rr, t(p).f[ll][rr]);
}
}
}
}
return ;
}
void build(int p, int l, int r) {
memset(t(p).f, 0x3f, sizeof(t(p).f));
l(p) = l, r(p) = r;
if(l == r) {
int val = (int)(ch[l] - 'a' + 1);
memset(t(p).f, 0x3f, sizeof(t(p).f));
for(int i = 1; i <= 5; i++) {
t(p).f[i][i] = cha(val, i);
// printf("l %d r %d .f[%d][%d] %d\n", l, r, i, i, t(p).f[i][i]);
}
return ;
}
int mid = (l + r) >> 1;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
update(p);
}
void change(int p, int l, int r, int val) {
if(l(p) == r(p)) {
memset(t(p).f, 0x3f, sizeof(t(p).f));
for(int i = 1; i <= 5; i++) {
t(p).f[i][i] = cha(val, i);
// printf("l %d r %d .f[%d][%d] %d val %d \n", l, r, i, i, t(p).f[i][i], val);
}
return ;
}
int mid = (l(p) + r(p)) / 2;
if(l <= mid) change(2 * p, l, r, val);
if(r > mid) change(2 * p + 1, l, r, val);
update(p);
return ;
}
int GET_ANS() {
int ans = 0x3f3f3f3f;
for(int i = 1; i <= 5; i++) {
for(int j = i; j <= 5; j++) {
// printf("f[%d][%d] %d\n", i, j, t(1).f[i][j]);
ans = min(ans, t(1).f[i][j]);
}
}
return ans;
}
void prework() {
memset(fp, 0x3f, sizeof(fp));
for(int i = 1; i <= 26; i++) {
fp[1][i] = cha(int(ch[1] - 'a' + 1), i);
}
for(int i = 2; i <= len; i++) {
for(int j = 1; j <= 26; j++) { //當前這位填啥
for(int k = 1; k <= j; k++) {
fp[i][j] = min(fp[i][j], fp[i - 1][k] + cha((int)(ch[i] - 'a' + 1), j));
}
}
}
}
int main() {
freopen("lock.in","r",stdin);
freopen("lock.out","w", stdout);
scanf("%s", ch + 1); len = strlen(ch + 1);
build(1, 1, len);
scanf("%d", &q);
if(q <= 10) {
prework();
int ans = 0x3f3f3f3f;
for(int i = 1; i <= 26; i++) {
ans = min(ans, fp[len][i]);
}
cout<<ans<<endl;
for(int i = 1; i <= q; i++) {
int id; cin>>id;
char chr; cin>>chr;
int ans = 0x3f3f3f3f;
ch[id] = chr;
int now = int(chr - 'a') + 1;
prework();
for(int j = 1; j <= 26; j++) {
ans = min(ans, fp[len][j]) ;
}
cout<<ans<<endl;
}
return 0;
}
int ans = GET_ANS();
cout<<ans<<endl;
for(int i = 1; i <= q; i++) {
int id; cin>>id;
char chr; cin>>chr;
int now = int(chr - 'a') + 1;
// cout<<"id "<<id<<" now "<<now<<endl;
change(1, id, id, now);
int ans = GET_ANS();
cout<<ans<<endl;
}
return 0;
}
100pts:問題轉化+性質
在全部數據中如果左右端點都有26種選擇,那么$26 * 262626$的巨大常數直接爆炸
發現問題可以轉化為將序列進行26次只含有(0/1)的最小代價之和,對于設$i$∈$[a,z]$分別將序列中的大于等于i的設為1,小于i的設為0,將代價累加起來即為答案
為什么這樣轉化問題是正確且保證最小 ?(不是很懂)
證明:
任務四的方法對于26的矩陣可能比較慢,設b(S,i)為一個長度為n的01串,第j個位置的值表示[S_j>=i],也就是當S_j>=i,該位置是1,否則是0。
那么對于b(S,i),可以當作只有a,b兩種字符的問題的求解。
令F(s)為s串的答案,下面證明:$\sum_{i='a'}^{'z'}F(b(S,i))=F(S)$
對于原串的一種最優解S',考慮對于一個位置j,有|S_j-S'j|個i在j位置是不同的,也就是有這么多i在該位置是有代價的。因此,對于任意一種最優解都有一種代價相等的將所有的b(S,i)變為b(S',i)的方案。因此$\sum_{i='a'}^{'z'}F(b(S,i))\le F(S)$
考慮對于每個i,b(S,i)的最佳答案。設$Z_i$為該答案中0的個數。
如果$Z_i$單調遞增,那么把i號字符放在$[Z_i,Z_i+1)$就能構造一種恰好答案相等的原串方案。
如果$Z_i$不單調遞增,假設$Z_i>Z_{i+1}$,那么交換i和i+1的方案不會使答案更劣,因為對于每個位置,b(S,i)和b(S,i+1)的對位情況只能有(1,0),(0,0),(1,1)。因此如果出現$Z_i>Z_{i+1}$,那么對位就出現了(0,1),一定是可以交換的。那么通過交換,可以得到一個$Z_i$單調遞增的方案。那么也就說明了
$\sum_{i='a'}^{'z'}F(b(S,i))\ge F(S)$
因此
$\sum_{i='a'}^{'z'}F(b(S,i))= F(S)$
對于每個01串分開做動態dp,要比26大小的矩陣做動態dp要更快,即可得到滿分**
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 1e5 + 70;
int fp[MAX][30];
int q, len;
char ch[MAX];
int cha(int x, int y) { //x -> y的最小花費
if(x < y) return y - x;
else return x - y;
}
struct SegmentTree {
int l, r;
int f[2][2]; //左 端點, 右端點分別填啥
#define l(x,y) tree[x][y].l
#define r(x,y) tree[x][y].r
#define t(x,y) tree[x][y]
}tree[30][MAX * 4];
void update(int p) {
for(int i = 1; i <= 26; i++) memset(t(i, p).f, 0x3f, sizeof(t(i, p).f));
for(int ro = 1; ro <= 26; ro++) {
for(int ll = 0; ll <= 1; ll++) {
for(int lr = ll; lr <= 1; lr++) {
for(int rl = lr; rl <= 1; rl++) {
for(int rr = rl ; rr <= 1; rr++) {
t(ro, p).f[ll][rr] = min(t(ro, p).f[ll][rr], t(ro, 2 * p).f[ll][lr] + t(ro, 2 * p + 1).f[rl][rr]);
}
}
}
}
}
return ;
}
void build(int p, int l, int r) {
for(int i = 1; i <= 26; i++) memset(t(i, p).f, 0x3f, sizeof(t(i, p).f));
for(int i = 1; i <= 26; i++) l(i, p) = l, r(i, p) = r;
if(l == r) {
for(int i = 1; i <= 26; i++) {
memset(t(i, p).f, 0x3f, sizeof(t(i, p).f));
int val = (ch[l] - 'a' + 1 >= i ? 1 : 0);
for(int j = 0; j <= 1; j++) t(i, p).f[j][j] = cha(val, j);
}
return ;
}
int mid = (l + r) >> 1;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
update(p);
}
void change(int p, int l, int r, int val) {
if(l(1, p) == r(1, p)) {
for(int i = 1; i <= 26; i++) {
memset(t(i, p).f, 0x3f, sizeof(t(i, p).f));
int V = (val >= i ? 1 : 0);
for(int j = 0; j <= 1; j++) t(i, p).f[j][j] = cha(V, j);
}
return ;
}
int mid = (l(1, p) + r(1, p)) / 2;
if(l <= mid) change(2 * p, l, r, val);
if(r > mid) change(2 * p + 1, l, r, val);
update(p);
return ;
}
int GET_ANS() {
int ans = 0;
for(int i = 1; i <= 26; i++) {
int sum = 0x3f3f3f3f;
for(int l = 0; l <= 1; l++) {
for(int r = l; r <= 1; r++)
sum = min(sum, t(i, 1).f[l][r]);
}
ans += sum;
}
return ans;
}
int main() {
freopen("lock.in","r",stdin);
freopen("lock.out","w", stdout);
scanf("%s", ch + 1); len = strlen(ch + 1);
build(1, 1, len);
scanf("%d", &q);
int ans = GET_ANS();
cout<<ans<<endl;
for(int i = 1; i <= q; i++) {
int id; cin>>id;
char chr; cin>>chr;
int now = int(chr - 'a') + 1;
change(1, id, id, now);
int ans = GET_ANS();
cout<<ans<<endl;
}
return 0;
}
魔法是變化之神
60pts:背包,求補集
將答案分成兩部分,第一部分為固定答案,第二部分為減少的總數,將$SIZ[]$看做體積,將減小量看做價值,背包即可拿到60pts
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 110000;
int n, m, ANS, siz[MAX], fat[MAX];
int tot, head[MAX], f[MAX];
struct made {
int l, t, val;
}edge[MAX *2], e[MAX *2];
void add(int u, int v, int val) {
edge[++tot].l = head[u];
edge[tot].t = v;
edge[tot].val = val;
head[u] = tot;
}
void dfs_pre(int x, int fa) {
fat[x] = fa;
siz[x] = 1;
for(int i = head[x]; i; i = edge[i].l) {
int t = edge[i].t;
if(t == fa) continue;
dfs_pre(t, x);
siz[x] += siz[t];
}
}
int main() {
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d%d", &n, &m);
for(int i = 1; i < n; i++) {
int u, v, val; scanf("%d%d%d", &u, &v, &val);
e[i].l = u, e[i].t = v, e[i].val = val;
add(u, v, val); add(v, u, val);
}
dfs_pre(1, 0);
for(int i = 1; i <= n; i++) {
int U = e[i].l;
if(fat[U] != e[i].t) U = e[i].t;
ANS += ((n - siz[U]) * (siz[U]) * e[i].val);
for(int j = m; j >= siz[U]; j--) {
f[j] = max(f[j], f[j - siz[U]] + (n - siz[U]) * (siz[U] * e[i].val));
}
}
cout<<ANS - f[m];
return 0;
}
100pts:考慮隨機數據
因為數據隨機,所以SIZ的期望個數有$log_{n}$中,因為邊的價值最多為5,將價值和$SIZ$相同的合并在一起,多重背包即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 110000;
int n, m;
LL ANS;
int siz[MAX], fat[MAX];
int tot, head[MAX];
LL f[MAX];
int cnt[MAX][10];
struct made {
int l, t, val;
}edge[MAX *2], e[MAX *2];
void add(int u, int v, int val) {
edge[++tot].l = head[u];
edge[tot].t = v;
edge[tot].val = val;
head[u] = tot;
}
void dfs_pre(int x, int fa) {
fat[x] = fa;
siz[x] = 1;
for(int i = head[x]; i; i = edge[i].l) {
int t = edge[i].t;
if(t == fa) continue;
dfs_pre(t, x);
siz[x] += siz[t];
}
}
void work() {
for(int i = 1; i < n; i++) {
int U = e[i].l;
if(fat[U] != e[i].t) U = e[i].t;
ANS += ((LL)(n - siz[U]) * (LL)(siz[U]) * (LL)e[i].val);
cnt[siz[U]][e[i].val]++;
}
}
inline int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar(); }
while(c >= '0' && c <= '9') { x = (x * 10 + f * (int)(c - '0')); c = getchar(); }
return x;
}
int main() {
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
n = read(), m = read();
// cout<<n<<endl;
for(int i = 1; i < n; i++) {
int u, v, val;
u = read(), v = read(), val = read();
e[i].l = u, e[i].t = v, e[i].val = val;
add(u, v, val); add(v, u, val);
}
dfs_pre(1, 0);
work();
for(int j = 100000; j >= 1; j--) {
for(int k = 5; k >= 1; k--) {
if(cnt[j][k] == 0) continue;
int C2 = 1;
while(C2 <= cnt[j][k]) {
LL W = C2 * (LL)j, val = (LL)C2 * k * j * (n - j);
for(int i = m; i >= W; i--)
f[i] = max(f[i], f[i - W] + val);
cnt[j][k] -= C2;
C2 *= 2;
}
if(cnt[j][k]) {
LL W = cnt[j][k] * j, val = (LL)cnt[j][k] * k * j * (n - j);
for(int i = m; i >= W; i--)
f[i] = max(f[i], f[i - W] + val);
}
}
}
cout<<ANS - f[m];
return 0;
}
奶牛的數學題:單位貢獻
正解
看題 看到數據范圍就應該想到這題是一道數學題,或者(矩陣乘法), 但顯然無法寫出線性遞推式,所以應該往數學上思考
1.如果一個數的$f(x) = i$, 則$x$最小為$lcm(1~i)$, 可得$f(x) <= 50$
2.將$num[i] * i = \sum _{i = 1}{i<=50}\sum_{j=i} num[j]$
- 所以題目轉化為了對于每一個$i \in50$ $f(x) \ge i$的個數, 如何計算個數,通過1可得,若$f(x) \ge i$則$x$必須為$lcm\left { 1,2,3...(i-1) \right }$的倍數,計算即可
#include<bits/stdc++.h> //將貢獻拆為單位貢獻
using namespace std;
#define LL long long
const int MAX = 1e4 + 70;
const int MOD = 1e9 + 7;
int main() {
freopen("math.in","r",stdin);
freopen("math.out","w",stdout);
int t; scanf("%d", &t);
while(t, t--) {
LL n; scanf("%lld", &n);
LL SUM = 1, ans = n % MOD;
for(int i = 2; i <= 50; i++) {
if(SUM > n) break;
ans = (ans + (n / SUM)) % MOD;
SUM = SUM * i / __gcd(SUM, (LL)i);
}
cout<<ans<<endl;
}
return 0;
}
路遇矩陣
 {
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
scanf("%d%d%d%d", &n, &m, &k, &p);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for(int i = 1; i <= n; i++) {
LL sum = 0;
for(int j = 1; j <= m; j++) sum += a[i][j];
h.insert(sum);
}
for(int j = 1; j <= m; j++) {
LL sum = 0;
for(int i = 1; i <= n; i++) sum += a[i][j];
l.insert(sum);
}
LL sum = 0;
for(int i = 0; i <= k; i++) { //控制刪i個行
H[i] = sum;
sum += *h.rbegin();
LL NOW = *h.rbegin();
set<LL>::iterator it = h.end();
it--;
h.erase(it);
h.insert(NOW - (LL)p * m);
}
sum = 0;
for(int i = 0; i <= k; i++) {
L[i] = sum;
sum += *l.rbegin();
LL NOW = *l.rbegin();
set<LL>::iterator it = l.end();
it--;
l.erase(it);
l.insert(NOW - (1LL * p * n));
}
for(int i = 0; i <= k; i++) {
ANS = max(ANS, L[i] + (H[k - i] - (1LL * p * i * (k - i))));
}
cout<<ANS<<endl;
return 0;
}
奶牛的括號匹配:狀壓
一眼狀壓,但是如何設計狀態呢?
首先套路的設定$f(i)$表示選定集合$i$所能產生的最大前綴匹配個數
但是若將$j$加入集合$i$必須滿足當前的$i$集合的最大方案是可以拓展的
那我們規定$f(i)$表示 $i$ 集合的最大答案,且當前的排列順序保證可以拓展 即 $($ 的個數 始終$\ge$ $)$
那么如何計算$j$對于集合$i$的貢獻,如果集合$i$的剩余 ${\color{Green} {\LARGE (} }$ 括號的個數為 $k$, 那么$j$所能貢獻的數量即為$cnt_jk$表示第$j$個串前綴${\color{Green} {\LARGE )} }$個數為$k$的數量位置
//狀壓DP, 狀態設計
// f[i] 表示集合i最大答案, 轉移
// 往i中添加新的字符串j, 考慮j的貢獻
// 若集合i匹配完仍剩OP個( 計算j的貢獻
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 22;
const int oo = 114514123;
int n, ans, cnt[MAX][410000]; //表示第i個串,出現j的個數
int f[(1 << MAX)];
int maxx[MAX], END[410000]; //表示第j個的最大的) 和 每個串最后的值
string s[MAX];
int num[(1 << MAX)]; //表示集合i的剩余(個數
void prework(int id, string S) {
int sum = 0;
maxx[id] = -oo;
for(int i = 0; i < S.size(); i++) {
if(S[i] == '(') sum--;
else sum++;
maxx[id] = max(maxx[id], sum);
if(sum >= maxx[id]) cnt[id][sum]++;
}
END[id] = sum;
}
int main() {
freopen("seq.in","r",stdin);
freopen("seq.out","w",stdout);
scanf("%d", &n);
for(int i = 0; i < n; i++) cin>>s[i]; // 第i個串
for(int i = 0; i < n; i++) prework(i, s[i]); //預處理第i個串的信息
for(int i = 0; i <= (1 << 21) - 1; i++) f[i] = -oo;
f[0] = 0;
for(int i = 0; i <= (1 << n) - 1; i++) { //枚舉 i集合
for(int j = 0; j < n; j++) { //選擇第j個填加
if( (i >> j ) & 1) continue;
if(num[i] >= maxx[j]) { //說明可以更新下一個f集合
f[i | (1 << j)] = max(f[i | (1 << j)], f[i] + cnt[j][num[i]]);
num[i | (1 << j)] = num[i] - END[j];
ans = max(ans, f[i | (1 << j)]);
} else { //不可以更新f集合但是可以統計答案
ans = max(ans , f[i] + cnt[j][num[i]]);
num[i | (1 << j)] = num[i] - END[j];
}
}
}
cout<<ans<<endl;
return 0;
}
潤不掉了:轉化點對問題
一步一步分析
20pts:爆搜,但是不好打
40pts
我們分析,如果對于當前的根為$root$
對于$root$的若干子樹,什么子樹需要貢獻1(被一個點看守)的答案呢?
那么應該是子樹$i$中的葉節點到$ro_i$的最小距離$\le$$dis(x,roi)$,
我們預處理葉子節點到其他點的最小距離,枚舉每一個點為根,向下遞歸答案即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 7e4 + 70;
int n, tot, head[MAX], du[MAX], len[MAX];
vector<int> son[MAX]; //
queue<int> q;
void BFS() {
while(!q.empty()) {
int now = q.front(); q.pop();
for(auto y : son[now]) {
if(len[y] > len[now] + 1) {
len[y] = len[now] + 1;
q.push(y);
}
}
}
}
int dfs(int now, int fa, int dis) {
if(len[now] <= dis) return 1;
if(du[now] == 1) return 1;
int sum = 0;
for(auto y : son[now]) {
if(y == fa) continue;
sum = sum + dfs(y, now, dis + 1);
}
return sum;
}
int main() {
freopen("run.in","r",stdin);
freopen("run.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
son[u].push_back(v);
son[v].push_back(u); //存邊
du[u] += 1; du[v] += 1;
}
memset(len, 0x3f, sizeof(len));
for(int i = 1; i <= n; i++) if(du[i] == 1) len[i] = 0, q.push(i);
BFS() ; // 處理
for(int i = 1; i <= n; i++) {
int ans = dfs(i, 0, 0) ; // 第i 個點向子樹跑
printf("%d\n", ans);
}
return 0;
}
100pts 子樹貢獻轉化
我們想,40pts沒有拓展性,因為無論怎樣,枚舉根的操作已經限制了整個算法,如何拓展呢?
我們想對于$x$有貢獻子樹的每個點都滿足$g(i) \le dis(i,x))$,
如果我們將整棵子樹的貢獻設為1的話,那么就是計算點對問題,顯然淀粉質就可以了
下面思考樹的子樹的性質
$\sum{du}=2siz-1$
變形
$\sum{du}-2siz=1$
$\sum{du-2}=1$
所以我們將每個點的val設為$du -2$對于點$x$的ans即為滿足
$g(i)\le dis(i,x)$的所有點的val之和
若$ro$為分治重心,統計過$ro$的點的答案
則$g(i)\le dis(ro,i)+dis(ro,x)$
則$g(i)-dis(ro,i)<=dis(ro,x)$
樹狀數組維護
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 7e4 + 70;
int n, tot, head[MAX], du[MAX], len[MAX], val[MAX];
bool v[MAX];
vector<int> son[MAX]; //
queue<int> q;
int ro, maxx_tr, f[MAX], dis[MAX], siz[MAX], g[MAX], NUM;
int tree[MAX << 1], ans[MAX];
int lowbit(int x) { return x & (-x); }
void add(int x, int val) { for(int i = x; i <= 2 * n; i += lowbit(i)) tree[i] += val; }
int Find(int x) { int sum = 0; for(int i = x; i; i -= lowbit(i)) sum += tree[i]; return sum; }
void get_root(int x, int fa) { //求重心
siz[x] = 1, f[x] = 0;
for(auto To : son[x]) {
if(To == fa || v[To]) continue;
get_root(To, x);
siz[x] += siz[To];
f[x] = max(f[x], siz[To]);
}
f[x] = max(f[x], NUM - siz[x]);
if(f[x] < maxx_tr) {
maxx_tr = f[x];
ro = x;
}
}
void Clear() { maxx_tr = n + 1; }
void get_size(int x, int fa) {
siz[x] = 1;
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
get_size(y, x);
siz[x] += siz[y];
}
}
void calc(int x, int fa) {
dis[x] = dis[fa] + 1;
ans[x] = ans[x] + Find(dis[x] + n); //加上n的偏移量
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
calc(y, x);
}
}
void change(int x, int fa, int zf) {
add(n + g[x] - dis[x], zf * val[x]);
for(auto y : son[x]) {
if(y == fa || v[y]) continue;
change(y, x, zf);
}
}
void work(int x, int id) {
dis[x] = 0;
if(id == 1) add(g[x] + n, val[x]); //端點有x,x的影響
for(auto y : son[x]) {
if(v[y]) continue;
calc(y, x); //統計當前這顆子樹的答案
change(y, x, 1); //將影響加上去
}
if(id == 1) ans[x] += Find(n);//計算端點值
for(auto y : son[x]) { //倒著做一次
if(v[y]) continue;
change(y, x, -1);
}
if(id == 1) add(g[x] + n, -val[x]);
}
void slove(int x) { //計算過x的點對之間的答案
v[x] = 1;
work(x, 1);
reverse(son[x].begin(), son[x].end());
work(x, 2);
for(auto y :son[x]) {
if(v[y]) continue;
Clear();
get_size(y, x);
NUM = siz[y];
get_root(y, x); //分治下去
slove(ro);
}
}
void BFS() {
while(!q.empty()) {
int now = q.front(); q.pop();
for(auto y : son[now]) {
if(g[y] > g[now] + 1) {
g[y] = g[now] + 1;
q.push(y);
}
}
}
}
int main() {
// freopen("run.in","r",stdin);
// freopen("run.out","w",stdout);
scanf("%d", &n);
for(int i = 1; i < n; i++) {
int u, v; scanf("%d%d", &u, &v);
son[u].push_back(v);
son[v].push_back(u);
du[u] += 1; du[v] += 1;
}
memset(g, 0x3f, sizeof(g));
for(int i = 1; i <= n; i++) if(du[i] == 1) q.push(i), g[i] = 0; //多源最短路
BFS();
for(int i = 1; i <= n; i++) val[i] = 2 - du[i]; //問題轉化,
Clear();
get_size(1, 0);
NUM = siz[1];
get_root(1, 0);
slove(ro);
for(int i = 1; i <= n; i++) {
if(du[i] == 1) printf("1\n");
else printf("%d\n", ans[i]);
}
return 0;
}
美好的查詢:神奇主席樹
80pts:分塊+并查集
不是很懂,先鍋著
100pts:神奇主席樹,多看看
數據范圍為$5e5$,也就是說我們希望得到一個$log$常數級別的程序,如何思考?
首先發現只有區間修改與區間查詢,而修改是將某一個范圍的固定值修改,且值不會大于$5e5$
如果我們在一顆線段樹上做的話,點權值不同無法統計且時間復雜度不能保證,我們按值分組
構建$5e5$棵線段樹,每一顆線段樹對應著一個值,初始將$ro[0]$的樹的每個節點都設為$1$,如果出現將某段區間加$1$操作,將$ro[x+1]$區間指向$ro[x]$的區間,但是我們會發現一個問題,細看下面的錯誤操作
如果我們按照上述數據依次操作,我們發現原本不屬于$ro[2]$的節點卻被歸到的$ro[2]$,也就是說我們對當前某一個值的修改會影響到下一個值,貌似很難處理,但實際很簡單,重新開一顆樹
這樣就很好解決了這個問題!為保證空間,采用動態開點
總結不出來什么啊
//在線, 5e5考慮log做法,值域主席樹 可維護,二分查最大值,復雜度n*log^2
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 5e5 + 80;
const int logMAX = 170;
int n, q, ro[MAX], tot;
struct made { int id, l, r, x; }ask[MAX];
struct SegmentTree {
int lson, rson, sum;
#define lson(x) tree[x].lson
#define rson(x) tree[x].rson
#define sum(x) tree[x].sum
}tree[MAX * logMAX];
int build() { return ++tot; }
void update(int p) { sum(p) = sum(lson(p)) + sum(rson(p)); }
void pre_build_0(int p, int l, int r) {
if(l == r) {
sum(p) = 1;
return ;
}
lson(p) = build();
rson(p) = build();
int mid = (l + r) >> 1;
pre_build_0(lson(p), l, mid);
pre_build_0(rson(p), mid + 1, r);
update(p);
}
int copy(int p, int q, int L, int R, int l, int r) {
if(L >= l && R <= r) { return p; } //為什么一定正確,反證法
if(p == 0) return p;
int now = build(); tree[now] = tree[q]; //復制
int mid = (L + R) >> 1;
if(mid >= l) lson(now) = copy(lson(p), lson(now), L, mid, l, r);
if(r > mid) rson(now) = copy(rson(p), rson(now), mid + 1, R, l, r);
update(now);
return now;
}
int qurry(int p, int L, int R, int l, int r) {
if(L >= l && R <= r) {
return sum(p);
}
int sum = 0;
int mid = (L + R) >> 1;
if(l <= mid) sum = sum + qurry(lson(p), L, mid, l, r);
if(r > mid) sum = sum + qurry(rson(p), mid + 1, R, l, r);
return sum;
}
int Find(int L, int R) {
int l = 0, r = 5e5, ans = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(qurry(ro[mid],1, n, L, R)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return ans;
}
int main() {
freopen("Innocent.in","r",stdin);
freopen("Innocent.out","w",stdout);
scanf("%d%d", &n, &q);
ro[0] = build();
pre_build_0(ro[0], 1, n); //建0的樹
for(int i = 1; i <= q; i++) {
int op; scanf("%d", &ask[i].id);
if(ask[i].id == 1) scanf("%d%d%d", &ask[i].l, &ask[i].r, &ask[i].x);
if(ask[i].id == 2) scanf("%d%d",&ask[i].l, &ask[i].r);
}
for(int i = 1; i <= q; i++) {
if(ask[i].id == 1) {
int now = copy(ro[ask[i].x], ro[ask[i].x + 1], 1, n, ask[i].l, ask[i].r); //保證空間為log
ro[ask[i].x + 1] = now;
}
else {
int ans = Find(ask[i].l, ask[i].r);
printf("%d\n", ans);
}
}
return 0;
}
新涂色游戲:操作反做
回頭看這道題其實非常簡單
對于涂色,整行或整列,則最后一定有一整行或一整列為一個顏色,反著做,再將操作reverse就可以了
//正著做不好做,反著刪
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define PII pair<int, int>
const int MAX = 1100;
int n, a[MAX][MAX], tot;
bool can[2 * MAX];
int num[2 * MAX][2 * MAX], kind[2 * MAX];
PII ans[2 * MAX];
void work(int id) {
if(id <= n) {
int co;
for(int i = 1; i <= n; i++) {
if(a[id][i] != 0 && a[id][i] != -1) {
co = a[id][i];
num[n + i][a[id][i]]--;
if(num[n + i][a[id][i]] == 0) kind[n + i] -= 1;
a[id][i] = 0;
}
}
ans[++tot] = {(PII){id, co}};
} else {
int co;
id = (id % n == 0) ? (n) : (id % n);
for(int i = 1; i <= n; i++) {
if(a[i][id] != 0 && a[i][id] != -1) {
co = a[i][id];
num[i][a[i][id]]--;
if(num[i][a[i][id]] == 0) kind[i] -= 1;
a[i][id] = 0;
}
}
ans[++tot] = {(PII){id + n, co}};
}
}
int main() {
freopen("game.in","r",stdin);
scanf("%d", &n);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++) {
scanf("%d", &a[i][j]);
if(a[i][j] == 0) a[i][j] = -1;
}
for(int i = 1; i <= n; i++) {
bool flg = 1;
for(int j = 1; j <= n; j++) {
if(a[i][j] == -1) {
flg = 0;
break;
} else {
if(num[i][a[i][j]] == 0) kind[i]++;
num[i][a[i][j]]++;
}
}
can[i] = flg;
}
for(int j = 1; j <= n; j++) {
bool flg = 1;
for(int i = 1; i <= n; i++) {
if(a[i][j] == -1) {
flg = 0;
break;
} else {
if(num[n + j][a[i][j]] == 0) kind[n + j]++;
num[n + j][a[i][j]]++;
}
}
can[n + j] = flg;
}
for(int i = 1; i <= 2 * n; i++) {
for(int j = 1; j <= 2 * n; j++) {
if(can[j] == 1 && kind[j] == 1) {
work(j);
can[j] = 0;
break;
}
}
}
reverse(ans + 1, ans + 1 + tot);
printf("%d\n", tot);
for(int i = 1; i <= tot; i++) {
printf("%d %d\n", ans[i].first, ans[i].second);
}
return 0;
}
新-滑動窗口簡單差分
典中典,$\sum li \le10^{6}$對于滑塊頂到最左邊與滑塊頂到最右邊相交的直接暴力做,如果不交,直接差分最大值,細節較多
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 1e6 + 70;
int n, w, l[MAX];
LL ans[MAX], cha[MAX];
LL a[MAX],b[MAX]; //統計
deque<LL> q;
void work(int h) {
LL maxx = 0;
vector<LL> Now;
Now.clear();
Now.push_back(0);
for(int j = 1; j <= l[h]; j++) {
LL x; scanf("%lld", &x);
b[j] = x;
maxx = max(maxx, x);
Now.push_back(x);
}
if(l[h] < w - l[h] + 1) { //不交
LL Max = 0;
for(int i = 1; i <= l[h]; i++) {
Max = max(Max, Now[i]);
ans[i] += Max;
}
Max = 0;
for(int i = w; i >= w - l[h] + 1; i--) {
Max = max(Max, Now[l[h] - (w - i)]);
ans[i] += Max;
}
cha[l[h] + 1] += maxx;
cha[w - l[h] + 1] -= maxx;
} else { //香蕉
memset(a, 0xcf, sizeof(a));
while(!q.empty()) q.pop_back();
int len = w - l[h] + 1;
for(int i = 1; i <= l[h]; i++) {
while(!q.empty() && i - q.front() + 1 > len) q.pop_front();
while(!q.empty() && Now[q.back()] < Now[i]) q.pop_back();
q.push_back(i);
a[i] = max(a[i], Now[q.front()]);
}
for(int i = 1; i <= w - l[h]; i++) a[i] = max(a[i], 1LL * 0);
while(!q.empty()) q.pop_back();
Now.clear();
for(int i = 0; i <= w - l[h]; i++) Now.push_back(0);
for(int i = w - l[h] + 1; i <= w; i++) Now.push_back(b[i - (w - l[h])]);
for(int i = w; i >= w - l[h] + 1; i--) {
while(!q.empty() && q.front() - i + 1 > len) q.pop_front();
while(!q.empty() && Now[q.back()] < Now[i]) q.pop_back();
q.push_back(i);
a[i] = max(a[i], Now[q.front()]);
}
for(int i = l[h] + 1; i <= w; i++) {
a[i] = max(a[i], 1LL * 0);
}
for(int i = 1; i <= w; i++) {
ans[i] += a[i];
}
}
}
int main() {
freopen("windows.in","r",stdin);
scanf("%d%d", &n, &w);
for(int i = 1; i <= n; i++) {
scanf("%d", &l[i]);
work(i);
}
for(int i = 1; i <= w; i++) {
cha[i] += cha[i - 1];
printf("%lld ", ans[i] + cha[i]);
}
return 0;
}
/*
1 5
2 -10 10
*/
小明去旅游 鍋
目前還不會,先鍋著
Heavy and Frail:分治優化重復操作
35pts,二進制分組背包暴力跑
80pts 背包合并
發現只有單點修改,其他不變,跑一個前綴背包,跑一個后綴背包,對于查詢,將前后兩個背包合并,再插入**
//根據m非常小的性質, 且是單點修改, 完全可以維護前i個數的背包,與后i個數的背包, 查詢時暴力合并,復雜度 m*m*q * logc
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 5100;
int n, m, q;
LL val[MAX], v[MAX], num[MAX];
LL f_pre[MAX][810], f_back[MAX][810]; //表示前i個數的背包, 后i個數的背包
LL f[810], ans[MAX * 10];
struct made {
int id;
LL x, y, z;
int whr;
}ask[MAX * 10];
bool mycmp(made X, made Y) {
return X.id < Y.id;
}
int main() {
freopen("reflect.in","r",stdin);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%lld",&val[i]);
for(int i = 1; i <= n; i++) scanf("%lld", &v[i]);
for(int i = 1; i <= n; i++) scanf("%lld", &num[i]);
scanf("%d", &q);
for(int i = 1; i <= q; i++) {
scanf("%d%lld%lld%lld", &ask[i].id, &ask[i].x, &ask[i].y, &ask[i].z);
ask[i].whr = i;
}
sort(ask + 1, ask + 1 + q, mycmp);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) f_pre[i][j] = f_pre[i - 1][j];
int Num = num[i], k = 1;
while(k <= Num) {
LL V = k * v[i]; LL VAL = k * val[i];
for(int j = m; j >= V; j--) f_pre[i][j] = max(f_pre[i][j], f_pre[i][j - V] + VAL);
Num -= k;
k *= 2;
}
if(Num != 0) {
LL V = Num * v[i]; LL VAL = Num * val[i];
for(int j = m; j >= V; j--) f_pre[i][j] = max(f_pre[i][j], f_pre[i][j - V] + VAL);
}
}
for(int i = n; i >= 1; i--) {
for(int j = 1; j <= m; j++) f_back[i][j] = f_back[i + 1][j];
int Num = num[i], k = 1;
while(k <= Num) {
LL V = k * v[i]; LL VAL = k * val[i];
for(int j = m; j >= V; j--) f_back[i][j] = max(f_back[i][j], f_back[i][j - V] + VAL);
Num -= k;
k *= 2;
}
if(Num != 0) {
LL V = Num * v[i]; LL VAL = Num * val[i];
for(int j = m; j >= V; j--) f_back[i][j] = max(f_back[i][j], f_back[i][j - V] + VAL);
}
}
for(int i = 1; i <= q; i++) {
for(int j = 1; j <= m; j++) f[j] = 0;
for(int j = 1; j <= m; j++) {
for(int k = 0; k <= j; k++) {
f[j] = max(f[j], f_pre[ask[i].id - 1][j - k] + f_back[ask[i].id + 1][k]);
}
}
int Num = ask[i].z, k = 1;
while(Num >= k) {
LL V = ask[i].y * k; LL VAL = ask[i].x * k;
for(int j = m; j >= V; j--) f[j] = max(f[j], f[j - V] + VAL);
Num -= k;
k *= 2;
}
if(Num) {
LL V = ask[i].y * Num; LL VAL = ask[i].x * Num;
for(int j = m; j >= V; j--) f[j] = max(f[j], f[j - V] + VAL);
}
ans[ask[i].whr] = f[m];
}
for(int i = 1; i <= q; i++) printf("%lld\n", ans[i]);
return 0;
}
100pts
只有單點修改,前后綴的合并$m^2$沒有拓展性,思考如果分治去做,$(l, r)$代表到$(l,r)$區間內除了$(l,r)$都已經被加入背包,到$(x, x)$時便統計答案,時間復雜度$O(nmlog_n^2+mq)$
重復計算,分治
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 5010;
int n, m, q;
LL val[MAX], v[MAX], num[MAX], ans[MAX * 10];
LL f[900];
struct made {
int id;
LL num, v, val;
};
vector<made> ask[MAX];
void Nowwork(int l, int r) {
for(int j = l; j <= r; j++) {
int Num = num[j], k = 1;
while(k <= Num) {
LL V = k * v[j], VAL = val[j] * k;
for(int j = m; j >= V; j--) f[j] = max(f[j], f[j - V] + VAL);
Num -= k;
k *= 2;
}
if(Num) {
LL V = Num * v[j], VAL = val[j] * Num;
for(int j = m; j >= V; j--) f[j] = max(f[j], f[j - V] + VAL);
}
}
}
void work(int x) {
LL fnow[802];
for(auto y : ask[x]) {
int Num = y.num, k = 1;
for(int i = 0; i <= m; i++) fnow[i] = f[i];
while(k <= Num) {
LL V = k * y.v, VAL = k * y.val;
for(int i = m; i >= V; i--) f[i] = max(f[i - V] + VAL, f[i]);
Num -= k;
k *= 2;
}
if(Num) {
LL V = Num * y.v, VAL = Num * y.val;
for(int i = m; i >= V; i--) f[i] = max(f[i - V] + VAL, f[i]);
}
ans[y.id] = f[m];
for(int i = 0; i <= m; i++) f[i] = fnow[i];
}
}
void slove(int l, int r) {
if(l == r) {
work(l);
return ;
}
int mid = (l + r) >> 1;
LL fnow[810];
for(int i = 0; i <= m; i++) fnow[i] = f[i];
Nowwork(mid + 1, r);
slove(l, mid);
for(int i = 0; i <= m; i++) f[i] = fnow[i];
Nowwork(l, mid);
slove(mid + 1, r);
}
int main() {
freopen("reflect.in","r",stdin);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%lld", &val[i]);
for(int i = 1; i <= n; i++) scanf("%lld", &v[i]);
for(int i = 1; i <= n; i++) scanf("%lld", &num[i]);
scanf("%d", &q);
for(int i = 1; i <= q; i++) {
int t; LL x, y, z; scanf("%d%lld%lld%lld", &t, &x, &y, &z);
made Now; Now.id = i, Now.num = z, Now.v = y, Now.val = x;
ask[t].push_back(Now);
}
slove(1, n);
for(int i = 1; i <= q; i++) {
printf("%lld\n", ans[i]);
}
return 0;
}
chess:根據題意分析
如果暴力DP,發現字符串比較在最劣情況下為$O(n)$,顯然過不了,考慮轉化問題,如果在 第$1$步就不是最優的策略一定不會被用到第$2$步,所以基于這個性質,我們可以考慮每一步最優為什么,將可以跑到最優的存入set,只用set里面的元素更新下一步的最優,這樣的時間復雜度就轉化為了$O((n + m)log)$
#include<bits/stdc++.h>
using namespace std;
const int MAX = 2100;
#define PII pair<int, int>
char ch[MAX][MAX];
char minn[MAX + MAX];
queue<PII> q;
set<PII> s[MAX + MAX];
int n, m;
int main() {
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin>>ch[i][j];
for(int i = 1; i <= n + m + 10; i++) minn[i] = 'z';
minn[1] = ch[1][1];
s[1].insert((PII){1, 1});
for(int i = 2; i <= n + m; i++) {
for(auto lst : s[i - 1]) {
int x = lst.first, y = lst.second;
if(x < n && minn[i] >= ch[x + 1][y]) {
if(minn[i] > ch[x + 1][y]) s[i].clear();
minn[i] = ch[x + 1][y];
s[i].insert((PII){x + 1, y});
}
if(y < m && minn[i] >= ch[x][y + 1]) {
if(minn[i] > ch[x][y + 1]) s[i].clear();
if(minn[i] > ch[x][y + 1]) minn[i] = ch[x][y + 1];
s[i].insert((PII){x, y + 1});
}
}
}
for(int i = 1; i <= n + m - 1; i++) {
cout<<minn[i];
}
return 0;
}
glass:簡單裝呀
數據范圍,一眼狀壓,壓什么? 我們發現一個瓶子只會被轉移一次,且轉移后一定不會再次轉移,所以我們就壓一個瓶子是否轉移過即可,時間復雜度$O(2^nnn)$跑不滿,所以能過
//一個瓶子只會被轉移
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = (1 << 21);
int n, k, ANS = 1e9;
int f[MAX], c[25][25];
int Minn[MAX][21];
int main() {
freopen("b.in","r",stdin);
freopen("b.out","w",stdout);
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) {
for(int j = 0; j < n; j++) {
cin>>c[i][j];
}
}
memset(Minn, 0x3f, sizeof(Minn));
for(int i = 0; i <= (1 << n) - 1; i++) {
for(int j = 0; j < n; j++) {
if(i >> j & 1) continue;
for(int k = 0; k < n; k++) {
if(k != j) if((i >> k & 1) == 0) Minn[i][j] = min(Minn[i][j], c[j][k]);
}
}
}
memset(f, 0x3f, sizeof(f));
f[0] = 0;
for(int i = 0; i <= (1 << n) - 1; i++) {
for(int j = 0; j < n; j++) {
if((i >> j & 1) == 0) {
f[i | (1 << j)] = min(f[i | (1 << j)], f[i] + Minn[i][j]);
}
}
}
int ans = 1e9;
for(int i = 0; i <= (1 << n); i++) {
int sum = 0;
for(int j = 0; j < n; j++) {
if((i >> j & 1) == 0) sum++;
}
if(sum == k) ans = min(ans, f[i]);
}
cout<<ans<<endl;
return 0;
}
card:組合意義的DP
原題,但是之前沒做,考場上也是一籌莫展
因為一個序列不同是操作順序有一位不同即可,所以一定有組合意義,考慮如何將一個問題轉化為一個組合問題
隨意構造一個序列,我們發現根據題意操作
在$1$左側的數下標一定遞減,在$1$右側的數下標遞增
那么最大值是怎么統計的呢? 一定為左側數的單調遞增加上右側數的單調遞增的個數
但是要規定左側的最大值,小于右側單調遞增的最小值
左側單調遞增在原序列中為以某個數(x)的單調遞減序列
為了保證上述規定,右側的單調遞增的數也由x為起點,這樣的話將總數減1即為最大嚴格遞增子序列的長度
而總數如何計算,假設對于$x$而言由若干個組合可以構成最大嚴格遞增子序列,對于其它的數除了1以外,往左放,往右放都無所謂,所以答案$+=sum*(2^{n-len+1}/2)1$
而求最長上升子序列,最長下降子序列需要一個$(log)$做法,離散化后值域線段樹即可
//一個序列不同,當且僅當操作序列不同, 具有計數優勢
//在1左邊的下標遞減, 值遞增, 在1右邊的下標遞增, 值遞增
//考慮用一個數劃分階段
#include<bits/stdc++.h>
using namespace std;
#define LL long long
//#define int long long
const int MOD = 1e9 + 7;
const int MAX = 2e5 + 70;
int n, a[MAX], b[MAX], tot;
LL num_up[MAX], num_down[MAX];
LL f_up[MAX], f_down[MAX];
int lsh[MAX];
struct node { int f, num; };
struct SegmentTree {
int l, r;
LL f, num;
#define l(x) tree[x].l
#define r(x) tree[x].r
#define f(x) tree[x].f
#define num(x) tree[x].num
}tree[MAX * 4];
LL quick_mi(int x, int y) {
LL xx = x, res = 1;
while(y) {
if(y & 1) res = res * xx % MOD;
xx = xx * xx % MOD;
y = y / 2;
}
return res;
}
void update(int p) {
if(f(2 * p) > f(2 * p + 1)) { f(p) = f(2 * p); num(p) = num(2 * p) % MOD; }
else if(f(2 * p + 1) > f(2 * p)) { f(p) = f(2 * p + 1); num(p) = num(2 * p + 1) % MOD; }
else if(f(2 * p + 1) == f(2 * p)) { f(p) = f(2 * p); num(p) = (num(2 * p) + num(2 * p + 1)) % MOD; }
return ;
}
void build(int p, int l, int r) {
l(p) = l, r(p) = r, f(p) = num(p) = 0;
if(l == r) { return ; }
int mid = (l + r) >> 1;
build(2 * p, l, mid);
build(2 * p + 1, mid + 1, r);
}
node New(node x, node y) {
if(x.f > y.f) return x;
else if(y.f > x.f) return y;
node NOW; NOW.f = x.f; NOW.num = (x.num + y.num) % MOD;
return NOW;
}
node Find(int p, int l, int r) {
if(l(p) >= l && r(p) <= r) {
node NOW; NOW.f = f(p); NOW.num = num(p);
return NOW;
}
node NOW; NOW.f = 0, NOW.num = 0;
int mid = (l(p) + r(p)) >> 1;
if(l <= mid) NOW = New(NOW, Find(2 * p, l, r));
if(r > mid) NOW = New(NOW, Find(2 * p + 1, l, r));
return NOW;
}
void change(int p, int l, int r, int ff, int num) {
if(l(p) == r(p)) {
if(ff > f(p)) {
f(p) = ff;
num(p) = num % MOD;
} else if(ff == f(p)) {
num(p) = (num(p) + num) % MOD;
}
return ;
}
int mid = (l(p) + r(p)) >> 1;
if(mid >= l) change(2 * p, l, r, ff, num);
if(r > mid) change(2 * p + 1, l, r, ff, num);
update(p);
}
void Clear() { for(int i = 0; i <= 8e5 + 1; i++) tree[i].f = tree[i].num = 0; }
signed main() {
freopen("c.in","r",stdin);
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]); b[i] = a[i];
}
sort(b + 1, b + 1 + n);
for(int i = 1; i <= n; i++) if(b[i] != b[i - 1]) lsh[++tot] = b[i];
for(int i = 1; i <= n; i++) a[i] = lower_bound(lsh + 1, lsh + 1 + tot, a[i]) - lsh;
reverse(a + 1, a + 1 + n);
build(1, 0, 2e5 + 1);
change(1, 2e5 + 1, 2e5 + 1, 0, 1);
for(int i = 1; i <= n; i++) {
node NOW = Find(1, a[i] + 1, 2e5 + 1);
f_up[i] = NOW.f + 1;
num_up[i] = NOW.num % MOD;
change(1, a[i], a[i], f_up[i], num_up[i]);
}
Clear();
change(1, 0, 0, 0, 1);
for(int i = 1; i <= n; i++) {
node NOW = Find(1, 0, a[i] - 1);
f_down[i] = NOW.f + 1;
num_down[i] = NOW.num % MOD;
change(1, a[i], a[i], f_down[i], num_down[i]);
}
LL maxx = 0, NUM = 0;
for(int i = 1; i <= n; i++) {
if(f_up[i] + f_down[i] - 1 > maxx) { maxx = f_up[i] + f_down[i] - 1; NUM = 0; }
if(f_up[i] + f_down[i] - 1 == maxx) NUM = (NUM + (1LL * num_up[i] * num_down[i] % MOD * quick_mi(2, n - f_up[i] - f_down[i] + 1) % MOD) ) % MOD;
}
cout<<maxx<<' '<<NUM<<endl;
return 0;
}
godnumber:鍋
ACAM套數位DP,還沒打
meirin:暴力化簡式子
簡單數學題?
首先不考慮增加操作,如果單求
$\sum_{l=1}^{n} \sum_{r=l}{n}(\sum_{j=l}a_i)(\sum_{j=l}^{r}b_i)$
考慮將里面的式子用前綴和表示出來即
$\sum_{l=1}^{n} \sum_{r=l}^{n}(Sa_r-Sa_{l-1})(Sb_r-Sb_{l-1})$
展開
$\sum_{l=1}^{n} \sum_{r=l}^{n}(Sa_rSb_r+Sa_{l-1}Sb_{l-1}-Sa_rSb_{l-1}-Sb{r}Sa_{l-1})$
令$S_{ab_i}$表示$Sa_i*Sb_i$,$SSa$表示$Sa$的前綴和,$SSb$表示$Sb$的前綴和
將式子中的$r$累加起來 化簡為$\sum_{i=1}^{n}S_{ab_i}n-Sa_{i-1}(SSb_{n}-SSb_{i-1})-Sb_{i-1}*(SSa_{n}-SSa_{i-1})$
這樣就得到了一個$NQ$ 時間復雜的的代碼,考慮如果有修改,考慮修改的貢獻
顯然題目中只對$b$進行操作,考慮每個$b$對應的$a$區間和
$\sum_{l=1}{i}\sum_{r=i}\sum_{j=l}^{r}a_i$
前綴優化一維
$\sum_{l=i}{i}\sum_{r=i}S_{a_r}-S_{a_{l-1}}$
將式子拆開,分別積掉一個$\sum$后再相加
$\sum_{i=1}^{n}i*(SS_{a_n}-SS_{a_{i-1}})-(n-i+1)(SS_{a_{i-1}})$
再對這個式子求前綴和即可$O1$計算貢獻,總時間復雜度$O(n+q)$
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define lll 1LL
const int MAX = 5e5 + 70;
const int MOD = 1e9 + 7;
int n, q;
int a[MAX], b[MAX], sa[MAX], ssa[MAX];
int g[MAX], sg[MAX];
LL ans;
int main() {
scanf("%d%d", &n, &q);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
for(int i = 1; i <= n; i++) scanf("%d", &b[i]);
for(int i = 1; i <= n; i++) sa[i] = (1LL * sa[i - 1] + 1LL * a[i] ) %MOD;
for(int i = 1; i <= n; i++) ssa[i] = (1LL * ssa[i - 1] + 1LL * sa[i]) % MOD;
for(int i = 1; i <= n; i++) {
g[i] = ((1LL * i * ((1LL * ssa[n] - 1LL * ssa[i - 1] + MOD) % MOD) - (1LL * (n - i + 1) * (1LL * ssa[i - 1])) ) + MOD ) % MOD;
}
for(int i = 1; i <= n; i++) sg[i] = (1LL * sg[i - 1] + g[i] + MOD) % MOD;
for(int i = 1; i <= n; i++) ans = (ans + (1LL * g[i] * b[i] % MOD) ) % MOD;
for(int i = 1; i <= q; i++) {
int l, r, k; scanf("%d%d%d", &l, &r, &k);
ans = (ans + ((1LL * sg[r] - sg[l - 1] + MOD) % MOD * 1LL * k) % MOD + MOD) % MOD;
printf("%lld\n", ans);
}
return 0;
}
sakuya:期望樹形DP好題
最討厭期望什么的了
首先不考慮修改
分析題意,要求期望難走程度,期望=概率*值
將值的貢獻拆分成若干點對的貢獻,發現每個點對在序列中相鄰的概率是相同的
題意變為 $\sum_{l=1}{m}\sum_{r=1}dis(l,r)(l \ne r)*P$
首先考慮$P$怎么計算, 發現$P$實際等于$\frac{2*(m-1)A_{m-2}{m-2}}{A_{m}{m}}$
接下來問題轉化為點對之間的貢獻如何計算,發現不好處理,考慮與上面相同的處理方式,將點對貢獻轉化為邊權*出現次數
發現出現次數可以用$f[v]*(m-f[v])$計算得出($f[i]表示以i為根的子樹中特殊點的個數$)
重新加回修改的限制
考慮對一個點的相連邊增加$k$對答案的增量為什么?為相連邊出現次數$num$×$k$
上述所有操作都可以在一次樹形DP中處理,時間復雜度$O(N+Q)$
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define LL long long
const int MAX = 5e5 + 70;
const int MOD = 998244353;
int tot, head[MAX], n, m, q, p[MAX], fa[MAX], f[MAX]; //表示以i為子樹的特殊點的個數
LL ans = 0;
int Num[MAX], sum[MAX];
vector<int> son[MAX];
vector<int> ce[MAX];
struct node { int u, v, val; }E[2 * MAX];
struct made { int l, t, id, val; }edge[MAX * 2];
void add(int u, int v, int id, int val) {
edge[++tot].l = head[u];
edge[tot].t = v;
edge[tot].val = val;
edge[tot].id = id;
head[u] = tot;
}
void dfs_pre(int x, int Fa) {
fa[x] = Fa;
f[x] = p[x];
for(int i = head[x]; i; i = edge[i].l) {
int to = edge[i].t;
if(to == Fa) continue;
ce[x].push_back(to);
son[x].push_back(to);
dfs_pre(to, x);
f[x] += f[to];
}
}
LL quick_mi(int x, int y) {
LL xx = x, res = 1;
while(y) {
if(y % 2) res = res * xx % MOD;
xx = xx * xx % MOD;
y /= 2;
}
return res % MOD;
}
signed main() {
scanf("%lld%lld", &n, &m);
for(int i = 1; i < n; i++) {
int u, v, val; scanf("%lld%lld%lld", &u, &v, &val);
E[i].u = u, E[i].v = v; E[i].val = val;
add(u, v, i, val); add(v, u, i, val);
}
for(int i = 1; i <= m; i++) {
int x; scanf("%lld", &x);
p[x] = 1;
}
LL P_up = 1, P_down = 1, P;
for(int i = 1; i <= m - 2; i++) P_up = (P_up * 1LL * i )% MOD;
for(int i = 1; i <= m; i++) P_down = (P_down * 1LL * i) % MOD;
P = 2LL * (m - 1) * P_up % MOD * quick_mi(P_down, MOD - 2) % MOD;
dfs_pre(1, 0); //兒子節點, 父親節點,相鄰的邊
for(int i = 1; i < n; i++) {
if(E[i].u != fa[E[i].v]) swap(E[i].u, E[i].v);
Num[E[i].v] = (f[E[i].v] * (m - f[E[i].v])) % MOD;
}
for(int i = 1; i < n; i++) {
if(E[i].u != fa[E[i].v]) swap(E[i].u, E[i].v);
ans = (ans + (E[i].val * Num[E[i].v] % MOD) ) % MOD;
}
for(int i = 1; i <= n; i++) {
sum[i] = (sum[i] + Num[i]) % MOD;
for(int j = 0; j < ce[i].size(); j++) sum[i] = (sum[i] + Num[ce[i][j]]) % MOD;
}
scanf("%lld", &q);
for(int i = 1; i <= q; i++) {
int x, k; scanf("%lld%lld", &x, &k);
ans = (ans + (1LL * sum[x] * k)) % MOD;
printf("%lld\n", (ans * P) % MOD);
}
return 0;
}
交換消消樂:簡單性質題
將貢獻拆成兩部分,一部分為消除貢獻顯然為$n$,另一部分為移動貢獻
考慮對于一個元素$i$,把$i$消掉需要多少步
設$i$左右端點分別為$l_i,r_i$
若只將$[l_i,r_i]$中元素移除區間,不考慮移動左右端點
如果將$i$這個元素消掉,則需要$[l_i,r_i]$中的出現次數為奇數的元素離開$[l_i,r_i]$的區間
通過打表或手玩發現,移動次數為所有$[l_i,r_i]$中出現奇數次的數的個數之和
這樣我們就得到了一個$n ^ 2$做法,發現統計奇數次出現數目不好統計,正難則反
用$r_i-l_i+1-2num_{mod2=0}$含義是區間長度減去出現偶數次的數的個數$2$即為出現奇數次個數
發現滿足$l_i<=l_j$和$r_i>=r_j$二維數點問題,樹狀數組維護即可
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int MAX = 5e5 + 70;
int n,val[2 * MAX];
LL ans = 0, tree[MAX * 2];
bool bol[MAX];
struct made {
int l, r;
}a[MAX];
bool mycmp(made X, made Y) { return X.l > Y.l; }
int lowbit(int x) { return x & (-x); }
LL Find(int x) {
LL res = 0;
for(int i = x; i; i -= lowbit(i)) res += tree[i];
return res;
}
void add(int x) {
for(int i = x; i <= 2 * n; i += lowbit(i)) tree[i] += 1;
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= 2 * n; i++) {
scanf("%d", &val[i]);
if(bol[val[i]] == 0) {
a[val[i]].l = i;
bol[val[i]] = 1;
} else a[val[i]].r = i;
}
sort(a + 1, a + 1 + n, mycmp);
for(int i = 1; i <= n; i++) {
LL res = Find(a[i].r);
ans = ans + (a[i].r - a[i].l - 1 - 2 * res);
add(a[i].r);
}
ans = ans / 2;
ans = ans + n;
printf("%lld\n", ans);
return 0;
}
[ABC232H] King's Tour:構造,鍋
Road of the King:神奇DP
神奇$DP$,希望得到一個$n^3$的做法
首先發現$1$能到達所有點, 所以若一個圖為強聯通分量,當且僅當所有點都能到達$1$
不妨設計$DP$狀態為$f[i][j][k]$表示已經走了$i$步,且經過了$j$個點,能夠到達$1$點的個數為$k$的方案數
我們想初始值如何賦,根據狀態可得$f[0][1][1] = 1$
轉移分三種情況
*1.若下一步前往了一個新的節點,得到轉移 : $f[i + 1][j + 1][k] += f[i][j][k] (n-j)$
2.若下一步重新去往了無法到達$1$的節點, 得到轉移 $f[i + 1][j][k]=f[i][j][k]*(j-k)$
3.若下一步去往了任意一個可以到達$1$的節點,則可以使所有不能到達$1$的節點全部變為可以到達, 得到轉移$f[i+1][j][j]=f[i][j][k]*k$
綜上即可解決問題
題目的分析其實非常巧妙,為何這樣設計狀態,為何這樣轉移一定是正確的
都可以從題目要求每次都從當前節點指向下一節點,起點為$1$ 這兩個限制條件,或者關鍵性質得出,所以要多注意題目的限制條件,設計與限制條件有關的$DP$狀態去解決問題
醉醉瘋瘋渺渺空空換根dp
經典題,考慮如何計數
兩點不在一條鏈上,發現如果能統計每個點子樹距離它的$\sum 2^{dis}$,此題就可做了
兩點在一條鏈上,發現需要統計$v$子樹外的點距離它的$\sum2^{dis}$$(dep[v]<dep[u])$,考慮求全集,然后減去$v$子樹中的$dis$之和即為子樹外的,然后全集就是換根求即可
F - Robot Rotation 不能隨機化的折半搜索
數據范圍提醒我們一定是一個優化后的指數級算法,考慮如何搜索.
根據題目的要求
我們發現$x$軸上移動只與奇數位的數字有關
$y$軸上的移動只與偶數位的數值大小有關
考慮$x$軸與$y$軸分離開,折半搜索可過
E - Revenge of "The Salary of AtCoder Inc." 期望
期望DP,感覺這塊太薄弱了
期望一般都是倒著做的
考慮設計狀態$f[i]$表示從 第$i$天為起點,直到結束的期望收益
發現$f[i]是由\sum f_j (j >i)$,然后發現可以前綴和優化,然后就做完了 ?
A. 1031模擬賽-A進步科學 狀壓DP
貌似可以稱作狀壓經典套路題
如果一次操作結束前不能使用其他的操作,我們發現一位狀態$f[S]$表示狀態為$S$的最小時間即可解決
目前唯一的難點在于在一次操作結束前可以使用其他操作,這也就表示了可能會有多種操作在同一時間同時進行
那么最直觀的感受當然是$f[S][T]$表示當前狀態為 $S$還有的操作序列為$T$的最小時間,但是我們發現時間仍然不對
首先我們發現總時間不會超過$2n$
那么上面竟然也說了時間很關鍵,考慮將操作序列抽象為一個關于時間的排列,若第$i$位為$0$即代表這一秒沒有操作,若第$i$秒為$j$,則代表對$j$進行操作
考慮將某一個時刻的操作抽象成一個二進制數,每次異或這個數即為影響,但這樣的時間復雜度為$20^{20}$
我們接著考慮優化,如果我們只記錄相同狀態的最小值就可以刪去很多無用信息
那么我們結合上面的,大膽設計狀態$f[t][S]$表示在$t$秒狀態為$S$是否合法,但是如果我們考慮向序列后面加數的話,無法保證結束時間,不妨往操作序列前面加數,這樣就類似枚舉了一個終止時間,然后就可以轉移了
時間復雜度$O(n2^n)$
B. 1031模擬賽-B吉吉沒急 差分約束
是一道轉換很巧妙的題
首先考慮用$-1$和$1$增加限制
先考慮$-1$,首先用若設計$f[x]$表示$x$最早什么時候可以學會
那么$-1$的點的$f[x]$都為正無窮
如果其他點和$-1$的點有連邊的話,那么最早只能在$L + 1$的時間連邊
那么如果我們發現$f[0]$比$0$大的話說明不合法
然后用$1$考慮增加限制,然后判斷每一個$1$是否合法
C. 1031模擬賽-C老杰克噠 動態DP
首先我們寫出轉移式
$a[i] = 0$
$\qquad$$f[i][1] = min(f[i - 1][0] + 1, f[i - 1][1] + 1);$
$\qquad f[i][0] = min(f[i - 1][0], f[i - 1][1] + 2);$
$a[i] = 1$
$\qquad f[i][1] = min(f[i - 1][0], f[i - 1][1]);$
$\qquad f[i][0] = min(f[i - 1][0] + 1, f[i - 1][1] + 2);$
我們發現轉移式是線性的且有修改,那么就是動態DP無疑了
動態DP構造矩陣時可以把行看成輸入值,把列看成輸出值
線段樹+矩陣維護一下
排座位 - 題目 - Daimayuan Online Judge] DP
如果沒有不能相鄰的限制的話,那么按照大小從小到大排序,兩兩配對即可得到最小值
接下來我們加入限制
下方黑色字代表的時排完序后的編號,上方綠色和紅色字體是排序前的編號,紫色數字是空的下標
首先我們發現,如果有一對數$(fis,sed)$它們排完序后仍然互斥,我們考慮把它們倆拆開,
當然最優方案是和$fis$前一個換一換,或者$sed$和后一個換一換
那么我們自然的設計$dp$狀態$f[i][0/1/2]$分別代表這一對數不換,前一個數換,后一個數換
那么轉移如下
$if(AT same organize(a[i],a[i-1]))$
$\qquad f[i][0] = 0x3f3f3f3f3f3f3f$
$\qquad f[i][1] = f[i-2][2]+abs(a[i]-a[i-2])$
$\qquad f[i][0] = 0x3f3f3f3f3f3f3f$
$\qquad f[i][2] = min(f[i - 2][2] + abs(a[i - 2].val - a[i + 1].val), min(f[i - 2][0], f[i - 2][1])+ abs(a[i - 1].val - a[i + 1].val));$
$else$
$\qquad f[i][0] = min(min(f[i - 2][0], f[i - 2][1]) + abs(a[i - 1].val a[i].val), f[i - 2][2] + abs(a[i - 2].val - a[i].val));$
A. 方塊游戲 - 題目 - 多校信息學訓練題庫
普及題場上切不掉,真是個菜狗啊
首先考慮套路,差分一下區間修改變為了單點修改
這時候我們發現題目的要求就變成了將差分序列變為一段連續$>1$的段和一段$<0$的段
維護一下就行了
B. 雪球 - 題目 - 多校信息學訓練題庫
細節題,想不出來真菜啊??
首先我們發現一個雪球最多能收集到的雪就是$r[i]-l[i]+1$,$r[i].l[i]$分別表示它左右兩個雪球
但是實際能收集多少呢?
對與左邊的雪,應該是它向左與$l[i]$向右的長度恰好相接的長度,
對于右邊的雪也是同理
然后我們發現每個雪球的移動的距離相同,不妨維護前$i$個時刻向左最長距離,向右最長距離
然后二分一下就行了,但是要注意細節,
因為有可能沒有相切的時刻,那我們只能找下一次恰好交的位置
這時候統計雪的量的時候,我們還要看一看下一次實際有效移動,是向哪個方向,統計的量不同
D. 不要FQ - 題目 - 多校信息學訓練題庫 動態DP
因為可以向上向下跑,所以按照列為階段
首先應該能夠想出基本的$DP$
$f[1][i]=f[1][i-1]a[1][i]+(f[0][i-1]a[0][i]*a[1][i])$
$f[0][i]=f[0][i-1]a[0][i]+(f[1][i-1]a[0][i]*a[1][i])$
如果對于$T>0$的情況,我們選擇直接構造一個$2*2$的矩陣
但是對于$T=0$的情況呢
我們不妨枚舉左端點
然后考慮前$i$個的 $sum$
得到轉移
$sum[i] = sum[i-1]+f[i-1][0]+f[i-1][1]$ (這里求的不包含第$i$位)
然后發現也是線性的,可以將矩陣變成$3*3$的就行了
23zr提高day9-美人魚 - 題目 - Zhengrui Online Judge (zhengruioi.com)
看完題解直接就懂了,非常套路
將區間排序
首先,如果區間互不交的話
顯然我們每次單點修改只會對一個區間有影響,這樣的話二分一下就行了
考慮區間有交
會對一些區間有影響,我們畫一些情況觀察一下,如果沒有區間被包含的情況下,修改的應該是一段連續的下標
但是如果區間有包含呢?
顯然被包含的區間沒有用,把它們刪除就可以了
然后區間修改線段樹維護即可
總結
- 上一篇: 【调度算法】并行机调度问题遗传算法
- 下一篇: 一文搞懂双链表