20190813-20190831 智商恢复做题乱记&几句话题解 V1.0

V2.0

最近做的题目就一起放这里好了

其实是想偷懒/cy

0813

UVA12235

sol 题目中的操作等价于从序列中提出不超过$k$个位置,剩下部分合成一个序列,然后对于拿出来的$k$个位置,每种颜色并成一段,在序列中找到与其颜色一致的颜色段插入在其旁边;如果这样的段不存在那么就放在最后面并使答案$+1$。那么答案就是“没有被提出来的部分构成的序列的颜色段数和”+“提取出来的所有位置的所有颜色中没有出现在没被提取出来的部分构成的序列内的颜色数量”。

考虑DP:设$f_{i,j,k,l}$表示考虑了前$i$个位置,已经提出了$j$个位置,未被提出的位置构成的序列的最后一个元素的颜色是$k$,已经出现在未被提出的位置构成的序列的颜色集合是$l$时最小的颜色段数,转移枚举第$i+1$个位置是否提出。复杂度$O(Tnk \Sigma 2^\Sigma) , \Sigma=8$。
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

int dp[2][103][9][1 << 8] , val[103] , N , K , Case;
void chkmin(int &x , int y){x = x < y ? x : y;}

int main(){
    while(N = read()){
        K = read(); int sum = 0 , pos = 0;
        for(int i = 1 ; i <= N ; ++i) sum |= 1 << (val[i] = read() - 25);
        memset(dp , 0x3f , sizeof(dp)); dp[0][0][8][0] = 0;
        for(int i = 1 ; i <= N ; ++i){
            memset(dp[pos ^ 1] , 0x3f , sizeof(dp[pos]));
            for(int j = 0 ; j <= K ; ++j)
                for(int k = 0 ; k <= 8 ; ++k)
                    for(int l = 0 ; l < 1 << 8 ; ++l)
                        if(dp[pos][j][k][l] <= 1e6){
                            chkmin(dp[pos ^ 1][j][val[i]][l | (1 << val[i])] , dp[pos][j][k][l] + (k != val[i]));
                            chkmin(dp[pos ^ 1][j + 1][k][l] , dp[pos][j][k][l]);
                        }
            pos ^= 1;
        }
        int ans = 1e9;
        for(int i = 0 ; i <= K ; ++i)
            for(int j = 0 ; j <= 8 ; ++j)
                for(int k = 0 ; k < 1 << 8 ; ++k)
                    chkmin(ans , dp[pos][i][j][k] + __builtin_popcount(sum ^ k));
        cout << "Case " << ++Case << ": " << ans << endl << endl;
    }
    return 0;
}

UVA12099

sol 暴力DP当然是记录三个书架的高度和宽度,但是注意到如果我们按照高度降序DP,那么每一个书架的高度会在第一本书放入之后被确定。这样我们可以优化成以下DP:

$f_{i,j,k,l}$表示放入了前$i$本书,三个书架的宽度分别是$j,k,l$的书架最小高度。注意到$j+k+l = \sum\limits_{x=1}^i width_x$,所以可以压掉一维,变成$f_{i,j,k}$。复杂度$O(Tn^3width^2)$看起来很不可过,但是你只需要在转移$f_i$的时候$j+k$只枚举到$\sum\limits_{x=1}^i width_x$就可以通过。关于空间的问题可以滚动数组解决。
#include
using namespace std;

struct book{
    int h , w;
    friend bool operator <(book a , book b){return a.h > b.h;}
}now[71];
int dp[2][2101][2101] , pre[71] , N , T;

int main(){
    for(cin >> T ; T ; --T){
        cin >> N;
        for(int i = 1 ; i <= N ; ++i) cin >> now[i].h >> now[i].w;
        sort(now + 1 , now + N + 1);
        for(int i = 1 ; i <= N ; ++i) pre[i] = pre[i - 1] + now[i].w;
        int pos = 0; memset(dp , 0x3f , sizeof(dp)); dp[0][0][0] = 0;
        for(int i = 1 ; i <= N ; ++i){
            memset(dp[pos ^ 1] , 0x3f , sizeof(dp[pos]));
            for(int j = 0 ; j <= pre[i - 1] ; ++j)
                for(int k = 0 ; k + j <= pre[i - 1] ; ++k)
                    if(dp[pos][j][k] != 0x3f3f3f3f){
                        dp[pos ^ 1][j + now[i].w][k] = min(dp[pos ^ 1][j + now[i].w][k] , dp[pos][j][k] + (!j) * now[i].h);
                        dp[pos ^ 1][j][k + now[i].w] = min(dp[pos ^ 1][j][k + now[i].w] , dp[pos][j][k] + (!k) * now[i].h);
                        dp[pos ^ 1][j][k] = min(dp[pos ^ 1][j][k] , dp[pos][j][k] + (pre[i - 1] == j + k) * now[i].h);
                    }
            pos ^= 1;
        }
        int ans = 1e9;
        for(int i = 1 ; i <= pre[N] ; ++i)
            for(int j = 1 ; j + i < pre[N] ; ++j)
                if(dp[pos][i][j] <= 1000)
                    ans = min(ans , dp[pos][i][j] * max(max(i , j) , pre[N] - i - j));
        cout << ans << endl;
    }
    return 0;
}

UVA1407

sol 设$f_{i,0/1,j}$表示从$i$的子树向下走,走了$j$个点,是否回到第$i$个点的最小代价,转移是一个树形背包。注意到$f_{i,0/1}$是单调的,所以每一次询问直接二分即可。
#include
using namespace std;

int read(){int a; scanf("%d" , &a); return a;}

#define PII pair < int , int >
int sz[503] , tmp[2][503] , dp[503][2][503] , N , Case; vector < PII > ch[503];
bool nroot[503];
void chkmin(int &x , int y){x = x < y ? x : y;}

void dfs(int x){
    dp[x][0][1] = dp[x][1][1] = 0; sz[x] = 1;
    for(int i = 0 ; i < ch[x].size() ; ++i){
        int t = ch[x][i].first; dfs(t);
        memcpy(tmp , dp[x] , sizeof(tmp));
        for(int j = 1 ; j <= sz[x] ; ++j)
            for(int k = 1 ; k <= sz[t] ; ++k){
                chkmin(tmp[0][j + k] , min(dp[x][1][j] + ch[x][i].second + dp[t][0][k] , dp[x][0][j] + 2 * ch[x][i].second + dp[t][1][k]));
                chkmin(tmp[1][j + k] , dp[x][1][j] + dp[t][1][k] + 2 * ch[x][i].second);
            }
        memcpy(dp[x] , tmp , sizeof(tmp)); sz[x] += sz[t];
    }
}

int main(){
    while(N = read()){
        cout << "Case " << ++Case << ":\n";
        memset(dp , 0x3f , sizeof(dp)); memset(nroot , 0 , sizeof(nroot));
        for(int i = 0 ; i < N ; ++i) ch[i].clear();
        for(int i = 1 ; i < N ; ++i){
            int x = read() , y = read() , d = read();
            ch[y].push_back(PII(x , d)); nroot[x] = 1;
        }
        int rt = 0 , Q = read(); while(nroot[rt]) ++rt; dfs(rt);
        while(Q--) printf("%d\n" , upper_bound(dp[rt][0] + 1 , dp[rt][0] + N + 1 , read()) - dp[rt][0] - 1);
    }
    return 0;
}

0814

NOI2009管道取珠

sol 题目相当于计算选择两种方案使得输出方式相同的方案数

故设$f_{i,j,k,l}$表示第一种方案在上面的管道pop了$i$个、下面的管道pop了$j$个;第二种方案在上面的管道pop了$k$个、下面的管道pop了$l$个的方案数。注意到$i+j=k+l$,所以可以省掉一维变成$O(n^3)$时空。但是空间仍然有些大所以考虑将状态变为$f_{i+j,i,k}$,这样第一维可以较为方便地滚动数组。
#include
using namespace std;

const int _ = 503 , MOD = 1024523;
char str1[_] , str2[_]; int L1 , L2 , dp[2][_][_];

int main(){
    scanf("%d %d %s %s" , &L1 , &L2 , str1 + 1 , str2 + 1);
    dp[0][0][0] = 1; int pos = 0;
    for(int i = 1 ; i <= L1 + L2 ; ++i){
        memset(dp[pos ^ 1] , 0 , sizeof(dp[pos]));
        for(int j = 0 ; j <= L1 ; ++j)
            for(int k = 0 ; k <= L1 ; ++k)
                if(dp[pos][j][k]){
                    if(i - j <= L2 && i - k <= L2 && str2[i - j] == str2[i - k])
                        dp[pos ^ 1][j][k] = (dp[pos ^ 1][j][k] + dp[pos][j][k]) % MOD;
                    if(j < L1 && k < L1 && str1[j + 1] == str1[k + 1])
                        dp[pos ^ 1][j + 1][k + 1] = (dp[pos ^ 1][j + 1][k + 1] + dp[pos][j][k]) % MOD;
                    if(j < L1 && i - k <= L2 && str1[j + 1] == str2[i - k])
                        dp[pos ^ 1][j + 1][k] = (dp[pos ^ 1][j + 1][k] + dp[pos][j][k]) % MOD;
                    if(i - j <= L2 && k < L1 && str2[i - j] == str1[k + 1])
                        dp[pos ^ 1][j][k + 1] = (dp[pos ^ 1][j][k + 1] + dp[pos][j][k]) % MOD;
                }
        pos ^= 1;
    }
    cout << dp[pos][L1][L1] << endl;
    return 0;
}

NOI2009二叉查找树

sol 最开始好像没有管权值能够变大这件事情然后写了个$O(n^3)$的DP只能过30= =

首先按照权值排列后,树的结构的建立就是选择一个权值最小的作为根,然后两边递归处理。可以发现区间DP模型:设$f_{i,j}$表示当树上节点只有下标在区间$[i,j]$上的节点时的最小权值。

注意到对于每一次转移枚举的根实际上有这些情况和转移:1、权值比根小,只能调大权值;2、权值比根大,可以不调整直接递归,让后面的部分进行调整;3、权值比根大但是调整权值到与根相同(可以把权值调整为实数的意义是对于权值相同的部分可以任选排列方式)。那么每一次的转移会和根的权值有关。故添加根的权值的状态:设$f_{i,j,k}$表示当前树上节点只有下标在区间$[i,j]$上的节点、且这一段子树的父节点的权值为$w$时的最小权值。

权值范围比较大但是注意到权值一定只会修改为给定的节点的权值,所以可以离散化压缩状态。复杂度$O(n^4)$。
#include
using namespace std;

struct num{
    int val , rnd , tms;
    friend bool operator <(num a , num b){return a.val < b.val;}
}now[75];
int dp[75][75][75] , sum[75] , lsh[75] , N , K;

int main(){
    cin >> N >> K;
    for(int i = 1 ; i <= N ; ++i) cin >> now[i].val;
    for(int i = 1 ; i <= N ; ++i){cin >> now[i].rnd; lsh[i] = now[i].rnd;}
    for(int i = 1 ; i <= N ; ++i) cin >> now[i].tms;
    sort(now + 1 , now + N + 1); sort(lsh + 1 , lsh + N + 1);
    for(int i = 1 ; i <= N ; ++i) now[i].rnd = lower_bound(lsh + 1 , lsh + N + 1 , now[i].rnd) - lsh;
    for(int i = 1 ; i <= N ; ++i) sum[i] = sum[i - 1] + now[i].tms;
    for(int i = N ; i ; --i)
        for(int j = i ; j <= N ; ++j)
            for(int k = 1 ; k <= N ; ++k){
                dp[i][j][k] = 2e9;
                for(int l = i ; l <= j ; ++l)
                    dp[i][j][k] = min(dp[i][j][k] , min(dp[i][l - 1][k] + dp[l + 1][j][k] + K ,
                                                        now[l].rnd >= k ? dp[i][l - 1][now[l].rnd] + dp[l + 1][j][now[l].rnd] : dp[i][j][k]));
                dp[i][j][k] += sum[j] - sum[i - 1];
            }
    cout << dp[1][N][1];
    return 0;
}

CQOI2012模拟工厂

sol 看到$n \leq 15$不难发现先要做的事情是$2^n$枚举选择的任务,那么我们只要快速check就行了。

按时间考虑每一个任务,很直观的一个贪心是能够加生产力就加生产力,以防之后的大供应量,但这很有可能导致一些任务因为没时间生产GG。所以我们只需要在让所有任务都不会因为生产时间过少而GG的情况下尽可能多升级生产力。

现在考虑第$i-1$个任务至第$i$个任务之间的升级生产力时间$x$,那么对于第$j$个任务,设$i-1$至$j$任务的时间差为$T$,$i$至$j$任务的需求总和为$w$,则如果剩下的$T-x$时间全部用来生产还不能满足需求$w$,且对于更大的$x$也不满足,那么当前的$x$就是不合法的。

数学化地描述就是考虑不等式$(T-x)(M+x) \geq w$,$M$是之前的生产力。如果它无解那么显然没有方案,否则如果它的解是$x1 \leq x \leq x2$,则当$x \leq x2$时都不会因为生产时间太少GG。所以对于所有的$x2$取min就可以知道当前应该升级多少生产力。
#include
using namespace std;

#define int long long
struct thing{
    int T , G , M;
    friend bool operator <(thing a , thing b){return a.T < b.T;}
}now[21];
int N;

int calc(int A , int B , int C){
    C -= A * B; A = B - A;
    if(A * A - 4 * C < 0) return -1e9;
    return floor((A + sqrt(A * A - 4 * C)) / 2);
}

signed main(){
    cin >> N;
    for(int i = 0 ; i < N ; ++i) cin >> now[i].T >> now[i].G >> now[i].M;
    sort(now , now + N); int ans = 0;
    for(int i = 1 ; i < 1 << N ; ++i){
        int lft = 0 , prc = 1 , pre = 0; bool flg = 1;
        for(int j = 0 ; j < N && flg ; ++j){
            if(i >> j & 1){
                int ans = 1e9 , sum = 0;
                for(int k = j ; ans >= 0 && k < N ; ++k)
                    if((i >> k & 1) && lft < (sum += now[k].G))
                        ans = min(ans , calc(prc , now[k].T - pre , sum - lft));
                flg &= ans >= 0;
                lft = lft + (now[j].T - pre - ans) * (prc += ans) - now[j].G;
                pre = now[j].T;
            }
        }
        if(flg){
            int sum = 0;
            for(int j = 0 ; j < N ; ++j) sum += (i >> j & 1) * now[j].M;
            ans = max(ans , sum);
        }
    }
    cout << ans; return 0;
}

UVA1437

sol 似乎和别人的做法有点不一样。设$str1,str2$是两个串。

先设$g_{i,j}$表示将$str1_{i,j}$变为空串,将$str1_{i,j}$变为$str2_{i,j}$的最小次数。因为一定要从$i$刷一次,而刷一次一定会有若干个位置满足之后都不会被刷。设这样的位置为$x_1,x_2,...,x_k$,其中$x_1=i$,那么考虑枚举$x_2$的位置,额外转移序列$x$只有一个元素的情况,有$g_{i,j} = \min\{1+g_{i+1,j} , \min\limits_{k\in[i+1,j] , str2_k = str2_i} g_{i+1,k-1}+g_{k,j}\}$。

又设$f_{i,j}$表示对于区间$[i,j]$将第一个串变为第二个串的最小次数,转移和上面类似,有一个额外的转移是$str1_i=str2_i$时$f_{i,j} = f_{i+1,j}$。

注意到一件事情:上述转移中序列$x_1,x_2,...,x_k$中可能存在某个位置$x_p(p \in (1,k))$满足$str1_{x_p} = str2_{x_p}$,但此时如果进行$f_{x_p,j} = f_{x_p+1,j}$的转移,那么这一次刷的$1$的代价就没算。故设$h_{i,j}$表示$f_{i,j}$不进行$f_{i,j} = f_{i+1,j}$转移的数组,在$str1_p = str2_p$的时候转移$h_{p,j}$,否则转移$f_{p,j}$。复杂度$O(Tn^3)$。
#include
using namespace std;

char str1[103] , str2[103]; int N , f[103][103] , g[103][103] , h[103][103];

int main(){
    while(scanf("%s %s" , str1 + 1 , str2 + 1) != EOF){
        N = strlen(str1 + 1);
        memset(f , 0 , sizeof(f)); memset(g , 0 , sizeof(g));
        for(int i = N ; i ; --i)
            for(int j = i ; j <= N ; ++j){
                g[i][j] = 1e9;
                for(int k = i ; k <= j ; ++k)
                    if(str2[i] == str2[k])
                        g[i][j] = min(g[i][j] , g[i + 1][k - 1] + (k == i ? g[k + 1][j] + 1 : g[k][j]));
            }
        for(int i = N ; i ; --i)
            for(int j = i ; j <= N ; ++j)
                if(str1[i] == str2[i]){
                    f[i][j] = f[i + 1][j]; h[i][j] = 1e9;
                    for(int k = i + 1 ; k <= j ; ++k)
                        if(str2[i] == str2[k]){
                            h[i][j] = min(h[i][j] , g[i + 1][k - 1] + (str2[k] == str1[k] ? h[k][j] : f[k][j]));
                        }
                }
                else{
                    f[i][j] = 1e9;
                    for(int k = i ; k <= j ; ++k)
                        if(str2[i] == str2[k])
                            f[i][j] = min(f[i][j] , g[i + 1][k - 1] + (k == i ? f[k + 1][j] + 1 : (str2[k] == str1[k] ? h[k][j] : f[k][j])));
                }
        cout << f[1][N] << endl;
    }
    return 0;
}

0815

HAOI2006数字序列

sol 对于第一问,注意到如果我们把第$i$个位置的值由$val_i$变成$val_i-i$,那么题目条件就会从“严格递增序列”变为“不降序列”。那么我们只需要求出最长不下降子序列就可以了。

对于第二问,注意到数据随机,考虑暴力(?)。对于位置$i$,我们考虑以其结尾的所有最长不降子序列中$i$之前的元素$j$,那么我们需要将$[j+1,i-1]$中的所有权值移动到$[val_j,val_i]$内。

一个结论是:必定存在一个$k \in [j,i-1]$,使得最优方案为$[j+1,k]$变为$val_j$,$[k+1,i-1]$变为$val_i$。证明考虑反证:设其中第一个不满足条件的段是$[p,q]$,那么我们统计$val_x , x \in [p,q]$中大于$val_i$的数的数量$cnt_1$和小于$val_j$的数的数量$cnt_2$。如果$cnt_1 > cnt_2$,那么可以把这一段的权值变为$val_{q+1}$,这样一定会使得总变化量变小;否则让这一段的权值变为$val_j$,那么变化量一定不增。

所以我们对于每个这样的$i,j$暴力枚举$k$即可。复杂度$O(\text{能过})$
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

#define int long long
const int _ = 40007;
int len[_] , dp[_] , val[_] , mn[_] , N , mx;
deque < int > id[_];

signed main(){
    N = read(); mn[0] = val[0] = -2e9;
    for(int i = 1 ; i <= N ; ++i) val[i] = read() - i;
    id[0].push_back(0); val[++N] = 2e9;
    for(int i = 1 ; i <= N ; ++i){
        int t = upper_bound(mn , mn + mx + 1 , val[i]) - mn;
        len[i] = t; mn[t] = val[i]; id[t].push_front(i); dp[i] = 2e9;
        mx = mx < t ? t : mx;
        for(int j = 0 ; j < id[t - 1].size() ; ++j){
            int p = id[t - 1][j] , sum = 0;
            if(val[p] > val[i]) break;
            for(int k = i - 1 ; k > p ; --k) sum += abs(val[i] - val[k]);
            for(int k = p ; k < i ; ++k , sum = sum + abs(val[p] - val[k]) - abs(val[k] - val[i]))
                dp[i] = min(dp[i] , dp[p] + sum);
        }
    }
    cout << N - len[N] << endl << dp[N];
    return 0;
}

AGC018C

sol 先考虑$Z=0$的情况,这是一个PJ组贪心问题,先强制所有人都选$B_i$,然后选择$X$个$A_i - B_i$最大的换成选$A_i$。

现在考虑$Z \neq 0$的情况,考虑按照上面的方法拓展。在最优方案中,如果将人按照$A_i - B_i$排序后得到一个序列,则最后选择$A_i$的位置一定比最先选择$B_i$的位置靠前,即我们枚举一个$k$,令序列中下标在$[1,k]$的选择$X$个$A_i$最大的选$A_i$,下标在$[k+1,X+Y+Z]$的选择$Y$个$B_i$最大的选$B_i$,其他选$C_i$。这用一个简单的数据结构就可以计算。

最后考虑如何消除$C_i$的影响。我们先强制所有位置都选择$C_i$,即将答案加上$\sum C_i$,$A_i -= C_i , B_i -= C_i$,就可以消除$C_i$的影响。
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

#define int long long
const int _ = 1e5 + 7;
struct peo{int A , B , C;}now[_]; int X , Y , Z , N , ans[_];

signed main(){
    freopen("in","r",stdin);
    X = read(); Y = read(); Z = read(); N = X + Y + Z;
    int sum = 0 , cur = 0;
    for(int i = 1 ; i <= N ; ++i){
        now[i].A = read(); now[i].B = read(); now[i].C = read();
        now[i].A -= now[i].C; now[i].B -= now[i].C; sum += now[i].C;
    }
    sort(now + 1 , now + N + 1 , [&](peo a , peo b){return a.A - a.B > b.A - b.B;});
    multiset < int > val;
    for(int i = 1 ; i <= N ; ++i){
        val.insert(now[i].A); cur += now[i].A;
        if(val.size() > X){cur -= *val.begin(); val.erase(val.begin());}
        ans[i] = cur;
    }
    val.clear(); cur = 0;
    for(int i = N ; i ; --i){
        val.insert(now[i].B); cur += now[i].B;
        if(val.size() > Y){cur -= *val.begin(); val.erase(val.begin());}
        ans[i - 1] += cur;
    }
    int mx = -1e18;
    for(int i = X ; i + Y <= N ; ++i) mx = max(mx , ans[i]);
    cout << sum + mx;
    return 0;
}

51NOD2589

sol duliuzsy

我们考虑当装备等级达到$x$时考虑$a_x$个人什么时候被打,如果当前的角色等级是$p$,那么在剩下的$n-x+n-p+\sum\limits_{i=x}^na_i$个时间点内,可以任意选择$a_x$个时间点打这些人。

考虑DP:设$f_{i,j}$表示角色等级为$i$、装备等级为$j$的方案数,转移考虑当前升级角色等级还是装备等级,升级装备等级则还需要乘上$\frac{(n-x+n-p+\sum\limits_{i=x}^na_i)!}{a_x!}$表示选择任意时间点打$a_x$个人。预处理阶乘做到$O(n^2)$。
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

const int MOD = 998244353;
int dp[5003][5003] , jc[5003 * 5003] , inv[5003 * 5003] , N , A[5003];

int poww(long long a , int b){
    int times = 1;
    while(b){
        if(b & 1) times = times * a % MOD;
        a = a * a % MOD; b >>= 1;
    }
    return times;
}

int add(int a , int b){return a + b >= MOD ? a + b - MOD : a + b;}
int calc(int a , int b){return 1ll * jc[a] * inv[a - b] % MOD;}

int main(){
    N = read();
    for(int i = 1 ; i <= N ; ++i) A[i] = read();
    for(int i = N ; i ; --i) A[i] += A[i + 1];
    dp[0][0] = jc[0] = 1;
    for(int i = 1 ; i <= (N + 2) * N ; ++i) jc[i] = 1ll * jc[i - 1] * i % MOD;
    inv[(N + 2) * N] = poww(jc[(N + 2) * N] , MOD - 2);
    for(int i = (N + 2) * N - 1 ; i >= 0 ; --i) inv[i] = inv[i + 1] * (i + 1ll) % MOD;
    for(int i = 0 ; i <= N ; ++i)
        for(int j = 0 ; j <= i ; ++j){
            dp[i + 1][j] = add(dp[i + 1][j] , dp[i][j]);
            dp[i][j + 1] = (dp[i][j + 1] + 1ll * dp[i][j] * calc(N - i + N - j - 1 + A[j + 1] , A[j + 1] - A[j + 2])) % MOD;
        }
    cout << dp[N][N];
    return 0;
}

LOJ6294

sol duliuzsy*2

看到$\gcd=1$考虑莫比乌斯反演,那么我们就是要计算对于每一个数$d$,满足路径边权$\gcd$是$d$的倍数的路径数量。这相当于计算路径上所有边都是$d$的倍数的路径数量。

先预处理对于每一个数$d$,满足路径边权是$d$的倍数的边。那么如果不考虑那条边权不确定的边,则将这些边连起来后,方案数就是$\sum_x \binom{sz_x}{2}$,其中$x$是一个连通块。

再考虑当边权不确定的边的边权是$d$的倍数时会产生怎样的额外贡献,不难发现就是$sz_p \times sz_q$,其中$p,q$是两个端点。于是直接枚举这条边的边权加上贡献即可。
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

#define int long long
#define PII pair < int , int >
#define st first
#define nd second
const int _ = 1e5 + 7;
int fa[_] , sz[_] , prm[_] , lst[_] , ans[_] , mu[_] , N , L , R , P , Q , sum , cnt;
bool nprm[_]; vector < PII > Ed[_];

int find(int x){return fa[x] == x ? x : (fa[x] = find(fa[x]));}
void merge(int x , int y){x = find(x); y = find(y); fa[x] = y; sz[y] += sz[x];}

signed main(){
    N = read(); L = read(); R = read(); P = read(); Q = read(); mu[1] = 1;
    for(int i = 2 ; i <= 1e5 ; ++i){
        if(!nprm[i]){prm[++cnt] = i; lst[i] = i; mu[i] = -1;}
        for(int j = 1 ; prm[j] * i <= 1e5 ; ++j){
            nprm[prm[j] * i] = 1; lst[prm[j] * i] = prm[j];
            if(i % prm[j] == 0) break;
            mu[prm[j] * i] = -mu[i];
        }
    }
    for(int i = 1 ; i <= N - 2 ; ++i){
        int A = read() , B = read() , L = read(); vector < int > in;
        while(L != 1){int l = lst[L]; in.push_back(l); while(L % l == 0) L /= l;}
        for(int j = 0 ; j < 1 << in.size() ; ++j){
            int tms = 1;
            for(int k = 0 ; k < in.size() ; ++k) tms *= (j >> k & 1 ? in[k] : 1);
            Ed[tms].push_back(PII(A , B));
        }
    }
    for(int i = 1 ; i <= 1e5 ; ++i)
        if(mu[i]){
            vector < int > nd; set < int > rt; sz[fa[P] = P] = sz[fa[Q] = Q] = 1;
            for(auto t : Ed[i]) sz[fa[t.st] = t.st] = sz[fa[t.nd] = t.nd] = 1;
            for(auto t : Ed[i]){merge(t.st , t.nd); nd.push_back(t.st); nd.push_back(t.nd);}
            sort(nd.begin() , nd.end()); nd.resize(unique(nd.begin() , nd.end()) - nd.begin());
            for(auto t : nd)
                if(rt.find(find(t)) == rt.end()){
                    rt.insert(find(t)); sum += mu[i] * sz[find(t)] * (sz[find(t)] - 1) / 2;
                }
            for(int j = i ; j <= 1e5 ; j += i) ans[j] += mu[i] * sz[find(P)] * sz[find(Q)];
        }
    for(int i = L ; i <= R ; ++i) printf("%lld\n" , ans[i] + sum);
    return 0;
}

0816

跳跳棋

sol 注意到任意一个状态如果向内跳,则至多只有1种方案。我们将某一个状态和(如果存在)它的一个向内跳得到的状态连边,那么得到的就是一棵二叉内向森林。

我们相当于要求某两个这样的状态在树上的距离,二分答案即可。
#include
using namespace std;

struct chess{
    int A , B , C;
    void sort(){if(A > B) swap(A , B); if(B > C) swap(B , C); if(A > B) swap(A , B);}
    friend bool operator !=(chess A , chess B){return A.A != B.A || A.B != B.B || A.C != B.C;}
}st , nd;

#define int long long
int work(chess &now , int lim = 1e12){
    int cnt = 0;
    while(lim > cnt && now.B - now.A != now.C - now.B){
        int l1 = now.B - now.A , l2 = now.C - now.B;
        if(l1 < l2){
            int t = min(lim - cnt , (l2 - 1) / l1); cnt += t;
            now.A += t * l1; now.B += t * l1;
        }
        else{
            int t = min(lim - cnt , (l1 - 1) / l2); cnt += t;
            now.C -= t * l2; now.B -= t * l2;
        }
    }
    return cnt;
}

signed main(){
    cin >> st.A >> st.B >> st.C >> nd.A >> nd.B >> nd.C;
    st.sort(); nd.sort(); chess tmp1 = st , tmp2 = nd;
    int cnt1 = work(tmp1) , cnt2 = work(tmp2);
    if(tmp1 != tmp2) puts("NO");
    else{
        puts("YES"); int L = 0 , R = cnt1;
        while(L < R){
            int mid = (L + R) >> 1; tmp1 = st; tmp2 = nd;
            work(tmp1 , mid); work(tmp2 , cnt2 - cnt1 + mid);
            tmp1 != tmp2 ? L = mid + 1 : R = mid;
        }
        cout << L + (cnt2 - cnt1 + L);
    }
    return 0;
}

LOJ6270

sol 降智打击

如果直接做似乎无法避免三维偏序或者离线之后的动态二维偏序,所以考虑在限制上下手。

考虑容斥,被$[L,R]$包含相当于总方案减去左端点小于$L$的数量减去右端点大于$R$的数量加上同时满足上述两个条件的数量。前三项都很好算,但是最后一项还是二维偏序。我们考虑消除最后一个部分的影响。注意到如果要满足这个条件,这个区间的长度一定比询问区间的长度长。我们考虑把最后一个条件变为$r_i - l_i \in [K , R - L]$,然后差分为$r_i - l_i \leq R - L$的答案减去$r_i - l_i \leq K - 1$的答案。

这样我们将这些询问按照$r_l - l_i$的限制从小到大排序,则对于每一个询问,满足这个条件的区间的长度一定会小于等于询问区间长度,这样上述容斥的最后一项就会变成$0$。那么对于第二项和第三项树状数组统计即可。
#include
using namespace std;

int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

#define int long long
#define PII pair < int , int >
const int _ = 5e5 + 7;
int N , M , Q , ans[_];
struct BIT{
#define lowbit(x) (x & -x)
    int arr[_];
    void add(int x , int num){while(x <= N){arr[x] += num; x += lowbit(x);}}
    int qry(int x){int sum = 0; while(x){sum += arr[x]; x -= lowbit(x);} return sum;}
}T1 , T2;
struct qry{int l , r , len , id , flg;}now[_ << 1];
vector < PII > inter[_];

signed main(){
    N = read(); M = read();
    for(int i = 1 ; i <= N ; ++i){
        int L = read() , R = read() , val = 1;
        inter[R - L].emplace_back(L , val);
    }
    for(int i = 1 ; i <= M ; ++i){
        int L = read() , R = read() , K = read();
        if(R - L >= K)
            now[++Q] = (qry){L , R , K - 1 , i , -1}, now[++Q] = (qry){L , R , R - L , i , 1};
    }
    sort(now + 1 , now + Q + 1 , [&](qry a , qry b){return a.len < b.len;}); int sum = 0 , pos = 0;
    for(int i = 0 ; i < N ; ++i){
        for(auto t : inter[i]){
            T1.add(t.first , t.second); T2.add(t.first + i , t.second); sum += t.second;
        }
        while(pos < Q && now[pos + 1].len == i){
            ++pos; ans[now[pos].id] += now[pos].flg * (T2.qry(now[pos].r) - T1.qry(now[pos].l - 1));
        }
    }
    for(int i = 1 ; i <= M ; ++i) printf("%lld\n" , ans[i]);
    return 0;
}

0817

Gym101981L

sol 我们设$X=0,Y=1,$其他的数都是$2$,那么我们需要做的就是每一次将某一个数移到序列的另一个位置,用最少的步数使得$01$不相邻。先做一些显然的特判。

考虑一个方案,那么这个移动方案中肯定会存在一个子序列,满足这个子序列中的数在方案中没有移动。如果这个序列的长度为$x$,那么答案就是$N-x$。我们考虑计算出最长的$x$,就可以得到最小的移动次数。

考虑怎样的子序列合法。我们给不移动的子序列的头尾加上一个$2$,不难注意到:1、当不移动的序列中存在相邻的$01$时,需要用一个可移动的$2$移动到中间;2、对于移动的$0$或$1$,如果在不移动的序列中存在$0$或$1$,就可以把这两个$0$或$1$并在一起,否则需要两个相邻的$2$把中间的位置让给这个数。后者情况最多只会在$0$或$1$中的一个出现,因为不移动的序列全都是$2$显然不优。

那么也就是说:(不移动的子序列的2的数量 + 不移动的子序列中相邻的01数量 + [在原序列中不存在位置给$0$或$1$])$\leq$ 2的总数量。

考虑DP:设$f_{i,j,k,l,m}$表示考虑前$i$个数,上面式子中的值为$j$,序列末尾元素为$k$,是否存在给$0/1$放的位置的最长序列长度。直接做的转移复杂度是$O(n^3)$的,但是注意到你的转移只会和当前位置和序列末尾的数有关,所以可以对$i$处理前缀最大值进行转移。
#include
using namespace std;
 
int mx[5003][3][2][2] , val[5003] , N , X , Y;
void chkmax(int &a , int b){a = a > b ? a : b;}
 
int main(){
    cin >> N >> X >> Y; int num2 = 0; bool flg1 = 0 , flg2 = 0;
    for(int i = 1 ; i <= N ; ++i){
        cin >> val[i];
        num2 += (val[i] = (val[i] == X ? 0 : (val[i] == Y ? 1 : 2))) == 2;
        flg1 |= val[i] == 0; flg2 |= val[i] == 1;
    }
    if(!flg1 || !flg2){puts("0"); return 0;}
    else if(!num2){puts("-1"); return 0;}
    memset(mx , -0x3f , sizeof(mx)); mx[0][2][0][0] = 0;
    for(int i = 1 ; i <= N ; ++i)
        for(int j = num2 ; j >= 0 ; --j){
            int tmp[3][2][2]; memset(tmp , -0x3f , sizeof(tmp));
            for(int k = 0 ; k <= 2 ; ++k)
                for(int l = 0 ; l <= 1 ; ++l)
                    for(int m = 0 ; m <= 1 ; ++m)
                        if(mx[j][k][l][m] >= 0){
                            int id1 = l | (val[i] == 0) | (k == 2 && val[i] == 2) ,
                                id2 = m | (val[i] == 1) | (k == 2 && val[i] == 2);
                            if(k + val[i] == 1 || val[i] == 2)
                                chkmax(mx[j + 1][val[i]][id1][id2] , mx[j][k][l][m] + 1);
                            else chkmax(tmp[val[i]][id1][id2] , mx[j][k][l][m] + 1);
                        }
            for(int k = 0 ; k <= 2 ; ++k)
                for(int l = 0 ; l <= 1 ; ++l)
                    for(int m = 0 ; m <= 1 ; ++m)
                        chkmax(mx[j][k][l][m] , tmp[k][l][m]);
        }
    int ans = 0;
    for(int i = 0 ; i <= num2 ; ++i)
        for(int j = 0 ; j <= 2 ; ++j)
            for(int k = 0 ; k <= 1 ; ++k)
                for(int l = 1 - k ; l <= 1 ; ++l)
                    if(i + (j == 2 ? 0 : !k | !l) <= num2)
                        chkmax(ans , mx[i][j][k][l]);
    printf("%d\n" , N - ans);
    return 0;
}

AGC037

日常只会AB然后跑路,题解在此

0818

全天打摆,啥事没干

0819

全天打摆,啥事没干

0820

IOI2018combo

sol 一件显然的事情是我们需要使用两次询问问出第一个字符,这样我们接下来可以使用第一个字符分隔多次询问。

然后我们接下来需要每一次询问问出接下来一个字符的方案。有一种构造是可行的:设当前询问出来的串是$S$,首字符是$A$,那么询问$SBSXBSXXSXY$,按照答案为$|S|-1,|S|,|S|+1$即可得到下一个字符是谁。最后使用两次询问问出最后一个字符即可,询问次数为$N+2$。
#include "combo.h"
#include
using namespace std;

string guess_sequence(int N){
    string ans; vector < string > ch = {"A" , "B" , "X" , "Y"};
    if(press(ch[0] + ch[1])) ans = press(ch[0]) ? ch[0] : ch[1];
    else ans = press(ch[2]) ? ch[2] : ch[3];
    if(N == 1) return ans;
    for(int i = 0 ; i < 4 ; ++i) if(ch[i] == ans){ch.erase(ch.begin() + i); break;}
    for(int i = 2 ; i <= N - 1 ; ++i)
        switch(press(ans + ch[0] + ans + ch[1] + ch[0] + ans + ch[1] + ch[1] + ans + ch[1] + ch[2]) - i){
        case -1: ans += ch[2]; break;
        case 0: ans += ch[0]; break;
        default: ans += ch[1]; break;
        }
    if(press(ans + ch[0]) == N) ans += ch[0];
    else if(press(ans + ch[1]) == N) ans += ch[1];
    else ans += ch[2];
    return ans;
}

CF Round580

VP了一下发现自己不会最小环了,于是BC基本上2h才切,三题中垫底的选手……

0821

CTSC2018混合果汁

sol 整体二分,每一次维护美味度$\geq mid$的所有果汁的线段树,线段树以价值为下标,节点存放的果汁容量。

每一次询问在线段树上二分可以得到当果汁容量恰好为$L$的时候的最小花费,就可以做出决策。
#include
using namespace std;

#define int long long
int read(){
    int a = 0; char c = getchar(); bool f = 0;
    while(!isdigit(c)){f = c == '-'; c = getchar();}
    while(isdigit(c)){
        a = a * 10 + c - 48; c = getchar();
    }
    return f ? -a : a;
}

const int _ = 1e5 + 7;
struct juice{int d , l , g;}now[_];
struct query{int id , g , L;}; vector < query > qry;
int N , M , ans[_];
namespace segtree{
    int sum1[_ << 2] , sum2[_ << 2];

#define mid ((l + r) >> 1)
#define lch (x << 1)
#define rch (x << 1 | 1)
    void mdy(int x , int l , int r , int d , int g){
        sum1[x] += g; sum2[x] += d * g; if(l == r) return;
        mid >= d ? mdy(lch , l , mid , d , g) : mdy(rch , mid + 1 , r , d , g);
    }

    int qry(int x , int l , int r , int tar){
        if(l == r) return sum1[x] < tar ? 2e18 : l * tar;
        if(sum1[lch] < tar) return qry(rch , mid + 1 , r , tar - sum1[lch]) + sum2[lch];
        return qry(lch , l , mid , tar);
    }
#undef mid
}using segtree::mdy;

void solve(vector < query > nowq , int L , int R){
    if(nowq.empty()) return;
    if(L == R){for(auto t : nowq) ans[t.id] = L; return;}
    vector < query > qL , qR; int mid = (L + R) >> 1;
    for(int i = mid + 1 ; i <= R ; ++i) mdy(1 , 0 , 1e5 , now[i].g , now[i].l);
    for(auto t : nowq) segtree::qry(1 , 0 , 1e5 , t.L) <= t.g ? qR.push_back(t) : qL.push_back(t);
    solve(qL , L , mid);
    for(int i = mid + 1 ; i <= R ; ++i) mdy(1 , 0 , 1e5 , now[i].g , -now[i].l);
    solve(qR , mid + 1 , R);
}

signed main(){
    N = read(); M = read(); qry.resize(M); now[0].d = -1;
    for(int i = 1 ; i <= N ; ++i){now[i].d = read(); now[i].g = read(); now[i].l = read();}
    sort(now + 1 , now + N + 1 , [&](juice A , juice B){return A.d < B.d;});
    for(int i = 0 ; i < M ; ++i){qry[i].id = i + 1; qry[i].g = read(); qry[i].L = read();}
    solve(qry , 0 , N); for(int i = 1 ; i <= M ; ++i) printf("%lld\n" , now[ans[i]].d);
    return 0;
}

0822

CF1132F

sol 跟string painter是一回事,设$dp_{i,j}$表示将区间$[i,j]$的所有字符删完的方案数,转移考虑删掉$i$的操作的删除序列$i,x_1,x_2,...,x_m$,那么$dp_{i,j} = \min\{dp_{i+1,j} + 1 , \min\limits_{x_1 \in [i+1,j] , str_{x_1} = str_i} dp_{i+1,x_1-1} + dp_{x_1 , j}\}$
#include
using namespace std;

int dp[503][503]; char str[503]; int N;

int main(){
    scanf("%d %s" , &N , str + 1);
    for(int i = N ; i ; --i)
        for(int j = i ; j <= N ; ++j){
            dp[i][j] = 1e9;
            for(int k = i ; k <= j ; ++k)
                if(str[i] == str[k])
                    dp[i][j] = min(dp[i][j] , dp[k + (i == k)][j] + dp[i + 1][k - 1] + (i == k));
        }
    cout << dp[1][N];
    return 0;
}

0823-0825

出去浪了啥事没干

0826

SP348

sol 拿个水题来更博

考虑我们第一次选择的加油站一定是在$P$范围内油量最多的,假设选择了这个加油站之后油量变为$P'$,那么如果$P' \geq L$就直接到达终点,否则就会选择除了第一次选择的加油站以外在$P'$范围内油量最多的加油站,……

那么不难想到贪心策略,因为$P$有单调性所以用一个指针+堆维护最优决策即可。
#include
#include
#include
using namespace std;

#define PII pair < int , int >

signed main(){
    ios::sync_with_stdio(0); int T;
    for(cin >> T ; T ; --T){
        vector < PII > fuel; priority_queue < int > q;
        int N , L , P , cnt = 0; cin >> N;
        for(int i = 1 ; i <= N ; ++i){int a , b; cin >> a >> b; fuel.push_back(PII(a , b));}
        cin >> L >> P; sort(fuel.begin() , fuel.end());
        int pos = fuel.size() - 1;
        while(pos >= 0 && L - fuel[pos].first <= P) q.push(fuel[pos--].second);
        while(!q.empty() && P < L){
            ++cnt; P += q.top(); q.pop();
            while(pos >= 0 && L - fuel[pos].first <= P) q.push(fuel[pos--].second);
        }
        cout << (P >= L ? cnt : -1) << endl;
    }
    return 0;
}

0827

HDU6279

sol 考虑我们在一个位置劈开环满足这个位置之后是黑色、这个位置之前是白色,那么环就会变成由相等段黑白色段相间组成的序列,且这个序列答案等于环的答案。

考虑先计算出序列的答案。把序列的黑白颜色分开,即可以通过计算:某种颜色的数量确定、段数确定时的所有方案贡献总和来求答案。

上述问题可以DP:设$f_{i,j}$表示长度为$i$的颜色序列划分成$j$段的贡献之和,有一个显然的$O(n^3)$转移,前缀和优化可以做到$O(n^2)$。那么对于一组询问$(N,M)$,序列上的答案就是$\sum\limits_{i=1}^{\min\{N , M\}} f_{N,i}f_{M,i}$。

再考虑环上的情况。一个很自然的想法是不断将序列的第一个元素放到最后一个构造环,也就是将答案乘上$N+M$。但是对于一个从某个满足条件的位置劈开之后存在$k$个黑白段的环来说,因为其存在$k$个劈开的位置满足条件,所以在计算序列贡献的时候这个环会被计算$k$次,所以在环上的答案就是

$(N+M)\sum\limits_{i=1}^{\min\{N , M\}} \frac{f_{N,i}f_{M,i}}{i}$
#include
using namespace std;

const int MOD = 1e9 + 7;
int f[5003][5003] , inv[5003] , T , N , M;
int add(int a , int b){return a + b >= MOD ? a + b - MOD : a + b;}

int poww(long long a , int b){
    int times = 1;
    while(b){
        if(b & 1) times = times * a % MOD;
        a = a * a % MOD; b >>= 1;
    }
    return times;
}

signed main(){
    for(int i = 1 ; i <= 5000 ; ++i) f[i][1] = i;
    for(int j = 2 ; j <= 5000 ; ++j){
        int sum = 0;
        for(int i = 1 ; i <= 5000 ; ++i){
            sum = add(sum , f[i - 1][j - 1]);
            f[i][j] = add(f[i - 1][j] , sum);
        }
    }
    for(int i = 1 ; i <= 5000 ; ++i) inv[i] = poww(i , MOD - 2);
    while(scanf("%d %d" , &N , &M) != EOF){
        int ans = 0;
        for(int i = 1 ; i <= N ; ++i) ans = (ans + 1ll * f[N][i] * f[M][i] % MOD * inv[i]) % MOD;
        printf("%lld\n" , 1ll * ans * (N + M) % MOD);
    }
    return 0;
}

Hrbust 1849

sol 考虑快速计算出每一个点时的答案。我们考虑计算每条边的贡献。

按照边权从大到小考虑每一条边,考虑到每条边时将边权比它大的所有边连上,那么这条边会产生贡献当且仅当中心设在这条边的两个端点所在的连通块内,且产生贡献的数量为另一个连通块的大小,对于其他的情况这条边一定不能产生贡献。

所以使用并查集对这样的策略进行维护,每一个连通块记录连通块大小和当前考虑的路径最小值,每一次合并更新即可。
#include
#include
using namespace std;

#define int long long
const int _ = 2e5 + 3;
int fa[_] , mx[_] , sz[_] , N;
struct Edge{int s , t , v;}now[_];
bool operator <(Edge A , Edge B){return A.v > B.v;}
int find(int x){return fa[x] == x ? x : (fa[x] = find(fa[x]));}

signed main(){
    while(scanf("%lld" , &N) != EOF){
        for(int i = 1 ; i < N ; ++i)
            scanf("%lld %lld %lld" , &now[i].s , &now[i].t , &now[i].v);
        sort(now + 1 , now + N);
        for(int i = 1 ; i <= N ; sz[i++] = 1) mx[fa[i] = i] = 0;
        for(int i = 1 ; i < N ; ++i){
            int p = find(now[i].s) , q = find(now[i].t);
            mx[p] = max(mx[p] + sz[q] * now[i].v , mx[q] + sz[p] * now[i].v);
            fa[q] = p; sz[p] += sz[q];
        }
        printf("%lld\n" , mx[find(1)]);
    }
    return 0;
}

0831

因为博主搞学科去了,所以这篇博客咕了完结撒花。

你可能感兴趣的:(20190813-20190831 智商恢复做题乱记&几句话题解 V1.0)