NOIP2019提高组 day2

1.Emiya 家今天的饭

题目描述

Emiya 是个擅长做菜的高中生,他共掌握 n n n 种烹饪方法,且会使用 m m m 种主要食材做菜。为了方便叙述,我们对烹饪方法从 1 ∼ n 1 \sim n 1n 编号,对主要食材从 1 ∼ m 1 \sim m 1m 编号。

Emiya 做的每道菜都将使用恰好一种烹饪方法与恰好一种主要食材。更具体地,Emiya 会做 a i , j a_{i,j} ai,j 道不同的使用烹饪方法 i i i 和主要食材 j j j 的菜 ( 1 ≤ i ≤ n , 1 ≤ j ≤ m ) (1 \leq i \leq n, 1 \leq j \leq m) 1in,1jm,这也意味着 Emiya 总共会做 ∑ i = 1 n ∑ j = 1 m a i , j \sum\limits_{i=1}^{n} \sum\limits_{j=1}^{m} a_{i,j} i=1nj=1mai,j道不同的菜。

Emiya 今天要准备一桌饭招待 Yazid 和 Rin 这对好朋友,然而三个人对菜的搭配有不同的要求,更具体地,对于一种包含 k k k 道菜的搭配方案而言:

Emiya 不会让大家饿肚子,所以将做至少一道菜,即 k ≥ 1 k \geq 1 k1
Rin 希望品尝不同烹饪方法做出的菜,因此她要求每道菜的烹饪方法互不相同
Yazid 不希望品尝太多同一食材做出的菜,因此他要求每种主要食材至多在一半的菜(即 ⌊ k 2 ⌋ \lfloor \frac{k}{2} \rfloor 2k道菜)中被使用
这里的 ⌊ x ⌋ \lfloor x \rfloor x 为下取整函数,表示不超过 x x x 的最大整数。

这些要求难不倒 Emiya,但他想知道共有多少种不同的符合要求的搭配方案。两种方案不同,当且仅当存在至少一道菜在一种方案中出现,而不在另一种方案中出现。

Emiya 找到了你,请你帮他计算,你只需要告诉他符合所有要求的搭配方案数对质数 998 , 244 , 353 998,244,353 998,244,353 取模的结果。

输入格式

第 1 行两个用单个空格隔开的整数 n , m n,m n,m

第 2 行至第 n + 1 n+1 n+1 行,每行 m m m 个用单个空格隔开的整数,其中第 i + 1 i+1 i+1 行的 m m m 个数依次为 a i , 1 , a i , 2 , ⋯   , a i , m a_{i,1}, a_{i,2}, \cdots, a_{i,m} ai,1,ai,2,,ai,m

输出格式

仅一行一个整数,表示所求方案数对 998 , 244 , 353 998,244,353 998,244,353 取模的结果

输入输出样例

输入 #1

2 3 
1 0 1
0 1 1

输出 #1

3

输入 #2

3 3
1 2 3
4 5 0
6 0 0

输出 #2

190

输入 #3

5 5
1 0 0 1 1
0 1 0 1 0
1 1 1 1 0
1 0 1 0 1
0 1 1 0 1

输出 #3

742

说明/提示

【样例 1 解释】

由于在这个样例中,对于每组 i , j i,j i,j,Emiya 都最多只会做一道菜,因此我们直接通过给出烹饪方法、主要食材的编号来描述一道菜。

符合要求的方案包括:

做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 2 的菜
做一道用烹饪方法 1、主要食材 1 的菜和一道用烹饪方法 2、主要食材 3 的菜
做一道用烹饪方法 1、主要食材 3 的菜和一道用烹饪方法 2、主要食材 2 的菜
因此输出结果为 3 m o d    998 , 244 , 353 = 3 3 \mod 998,244,353 = 3 3mod998,244,353=3。 需要注意的是,所有只包含一道菜的方案都是不符合要求的,因为唯一的主要食材在超过一半的菜中出现,这不满足 Yazid 的要求。

【样例 2 解释】

Emiya 必须至少做 2 道菜。

做 2 道菜的符合要求的方案数为 100。

做 3 道菜的符合要求的方案数为 90。

因此符合要求的方案数为 100 + 90 = 190。

【数据范围】

在这里插入图片描述

对于所有测试点,保证 1 ≤ n ≤ 100 1 \leq n \leq 100 1n100 1 ≤ m ≤ 2000 1 \leq m \leq 2000 1m2000 0 ≤ a i , j < 998 , 244 , 353 0 \leq a_{i,j} \lt 998,244,353 0ai,j<998,244,353

分析

纯暴力(32 pts)

  • 开始的第一思路,可以过测试点1-8
  • 直接枚举出每种烹饪方法所用的主要食材
  • 用搜索枚举,传入两个参数:现在枚举的烹饪方法编号以及在此之前的方案数
  • 代码:
#include<bits/stdc++.h>
const long long mod=998244353;
using namespace std;
int n,m;
int a[105][2005];
int cnt[2005];
long long ans;
void input(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&a[i][j]);
        }
    }
}
void dfs(int x,long long sum){
    if (x>n){
        long long num=0 ;
        for(int i=1;i<=m;i++){
            num+=cnt[i];
        }
        num/=2 ;
        for(int i=1;i<=m;i++){
            if(cnt[i]>num)
                return;
        }
        ans+=sum;
        ans=ans%mod;
        return;
    }
    dfs(x+1,sum);
    for(int i=1;i<=m;i++){
        if(a[x][i]==0)
            continue;
        cnt[i]++;
        dfs(x+1,sum*a[x][i]%mod);
        cnt[i]--;
    }
}
int main(){
    input();
    dfs(1,1);
    printf("%lld\n",ans-1);
    return 0;
} 

用dp过9-12(48pts)

  • 在写完32pts后,想先考虑m = 2(显然,m = 2会比其他简单),发现只有两种主要食材,所以必须选第一种食材的次数等于选第二种食材的次数
  • 想到包裹分拣,可以开三位分别表示现在决策的烹饪方法编号、现在选的主要食材一的个数以及现在选的主要食材二的个数
  • 之后动态规划一下就可以了
  • 代码:
#include<bits/stdc++.h>
const long long mod=998244353;
using namespace std;
int n,m;
int a[105][2005];
int cnt[2005];
long long ans;
long long dp[105][55][55];
void input() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&a[i][j]);
        }
    }
}
void dfs(int x,long long sum){
    if (x>n){
        long long num=0;
        for(int i=1;i<=m;i++){
            num+=cnt[i];
        }
        num/=2;
        for(int  i=1;i<=m;i++){
            if(cnt[i]>num)
                return;
        }
        ans+=sum;
        ans=ans%mod;
        return;
    }
    dfs(x+1,sum);
    for(int i=1;i<=m;i++){
        if(a[x][i]==0)
            continue;
        cnt[i]++;
        dfs(x+1,sum*a[x][i]%mod);
        cnt[i]--;
    }
}

int main(){
    input();
    if(n<=10){
        dfs(1,1);
        printf("%lld\n",ans-1);
        return 0;
    }
    if(m==2){
        ans=0;
        dp[0][0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j<=n/2;j++){
                for(int k=0;k<=n/2;k++){
                    dp[i][j][k]=dp[i-1][j][k];
                    if(j>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j-1][k]*a[i][1])%mod)%mod; 
                    }
                    if(k>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j][k-1]*a[i][2])%mod)%mod; 
                    }
                }
            }
        }
        for(int i=1;i<=n/2;i++){
            ans=(ans+dp[n][i][i])%mod;
        }
        printf("%lld\n",ans);
        return 0;
    }
    return 0;
}

m = 3(64pts)

  • 由m = 2的dp想到m = 3也可用dp,开四维处理即可
  • 直接dp一下即可
  • 代码几乎与m = 2相同
#include<bits/stdc++.h>
const long long mod=998244353;
using namespace std;
int n,m;
int a[105][2005];
int cnt[2005];
long long ans;
long long dp[105][55][55];
long long dp2[105][55][55][55];
void input() {
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&a[i][j]);
        }
    }
}
void dfs(int x,long long sum){
    if(x>n){
        long long num=0;
        for(int i=1;i<=m;i++){
            num+=cnt[i];
        }
        num/=2;
        for(int i=1;i<=m;i++){
            if(cnt[i]>num)
                return;
        }
        ans+=sum;
        ans=ans%mod;
        return;
    }
    dfs(x+1,sum);
    for(int i=1;i<=m;i++){
        if(a[x][i]==0)
            continue;
        cnt[i]++;
        dfs(x+1,sum*a[x][i]%mod);
        cnt[i]--;
    }
}
int main(){
    input();
    if(n<=10){
        dfs(1,1);
        printf("%lld\n",ans-1);
        return 0;
    }
    if(m==2){
        ans=0;
        dp[0][0][0]=1;
        for(int i=1;i<=n;i++){
            for(int j=0;j<=n/2;j++){
                for(int k=0;k<=n/2;k++){
                    dp[i][j][k]=dp[i-1][j][k];
                    if(j>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j-1][k]*a[i][1])%mod)%mod; 
                    }
                    if(k>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j][k-1]*a[i][2])%mod)%mod ; 
                    }
                }
            }
        }
        for(int i=1;i<=n/2;i++){
            ans=(ans+dp[n][i][i])%mod;
        }
        printf("%lld\n",ans);
        return 0;
    }
    else if(m==3){
        dp2[0][0][0][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=n;j++)
                for(int k=0;k<=i-j;k++)
                    for(int l=0;l<=i-j-k;l++){
                        dp2[i][j][k][l]=dp2[i-1][j][k][l];
                        if(j>=1) 
                            dp2[i][j][k][l]+=dp2[i-1][j-1][k][l]*a[i][1]%mod;
                        if(k>=1) 
                            dp2[i][j][k][l]+=dp2[i-1][j][k-1][l]*a[i][2]%mod;
                        if(l>=1) 
                            dp2[i][j][k][l]+=dp2[i-1][j][k][l-1]*a[i][3]%mod;
                    }
        for(int i=0;i<=n/2;i++) 
            for(int j=0;j<=n/2;j++) 
                for(int k=0;k<=n/2;k++){
                    if(i<=j+k&&j<=i+k&&k<=i+j) 
                        ans+=dp2[n][i][j][k];
                    ans=ans%mod;
                }
        printf("%lld\n",ans-1);
    }
    return 0;
} 

n = 40 && m = 500 (84pts)

  • 发现Yazid的条件是相对较难做到,从Yazid的条件下手讨论
  • 做完64pts后,感觉不太容易用顺向思维p做(m = 500肯定会炸空间)
  • 于是想到逆推(以)考虑,这是先算出总方案(定义sum[i]表示用第i种烹饪方法所能做出的总方案数,则总方案数为(sum[1] + 1)×(sum[2] + 1)× …… ×(sum[n] + 1))
  • 如果不满足Yazid的条件,则必然有一种菜品作为主要食材用了至少n / 2 + 1次,枚举这种菜品,之后将- 菜品分为主菜(即枚举出的菜品)与非主菜,显然这其中每类没有交集,所以可直接求出不满足Yazid的条件的总和,相减再减一即可
  • 其中枚举过程中只要用m = 2的解法即可
  • 代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353; 
const int N=2100;
int n,m;
int x[105][2005];
long long sum[105];
long long a[2005],b[2005];
long long dp[105][105][105];
void input(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&x[i][j]);
        }
    } 
    return;
}
void doit(){
    long long tot=1;
    for(int i=1;i<=n;i++){
        sum[i]=0;
        for(int j=1;j<=m;j++){
            sum[i]+=x[i][j];
            sum[i]%=mod;
        }
        tot=(tot*(sum[i]+1))%mod;
    }
    long long wrong=0;
    for(int food=1;food<=m;food++){
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
            a[i]=x[i][food],b[i]=(sum[i]-a[i]+mod)%mod;
        dp[0][0][0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=i;j++)
                for(int k=0;k<=i-j;k++){
                    dp[i][j][k]=dp[i-1][j][k];
                    if(j>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j-1][k]*a[i])%mod)%mod; 
                    }
                    if(k>0){
                        dp[i][j][k]=(dp[i][j][k]+(dp[i-1][j][k-1]*b[i])%mod)%mod;
                    }
                }
        for(int i=1;i<=n;i++)
            for(int j=0;j<=n-i;j++)
                if(i>j)
                    wrong+=dp[n][i][j];
        wrong%=mod;
    }
    printf("%lld\n",(tot-1-wrong+mod)%mod);
} 
int main(){
    input();
    doit();
    return 0;
} 

100pts

  • 思路与84pts的几乎一样,只是发现dp其实关心的只有差,所以可以用差值dp优化为O(n²),总复杂度O(n³)
  • 注意差值dp时必须平移,因为可能差为负数
  • 代码:
#include<bits/stdc++.h>
using namespace std;
const int mod=998244353; 
const int N=2100;
int n,m;
int x[105][2005];
long long sum[105];
long long a[2005],b[2005];
long long dp[105][2105];
void input(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            scanf("%d",&x[i][j]);
        }
    } 
    return;
}
void doit(){
    long long tot=1;
    for(int i=1;i<=n;i++){
        sum[i]=0;
        for(int j=1;j<=m;j++){
            sum[i]+=x[i][j];
            sum[i]%=mod;
        }
        tot=(tot*(sum[i]+1))%mod;
    }
    long long wrong=0;
    for(int food=1;food<=m;food++){
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
            a[i]=x[i][food],b[i]=(sum[i]-a[i]+mod)%mod;
        dp[0][N]=1;
        for(int i=1;i<=n;i++){
            for(int j=-1*i;j<=i;j++){
                dp[i][N+j]=dp[i-1][N+j];
                if(N+j-1>=0)
                    dp[i][N+j]+=(dp[i-1][N+j-1]*a[i]%mod);
                dp[i][N+j]+=(dp[i-1][N+j+1]*b[i]%mod);
                dp[i][N+j]%=mod ; 
            }
        }
        for(int i=1;i<=n;i++)
            wrong+=dp[n][N+i];
        wrong%=mod;
    }
    printf("%lld\n",(tot-1-wrong+mod)%mod);
} 
int main(){
    input();
    doit();
    return 0;
} 
} 

2.划分

题目描述

2048 年,第三十届 CSP 认证的考场上,作为选手的小明打开了第一题。这个题的样例有 n n n 组数据,数据从 1 ∼ n 1 \sim n 1n 编号, i i i 号数据的规模为 a i a_i ai

小明对该题设计出了一个暴力程序,对于一组规模为 uu 的数据,该程序的运行时间为 u 2 u^2 u2。然而这个程序运行完一组规模为 u u u 的数据之后,它将在任何一组规模小于 u u u 的数据上运行错误。样例中的 a i a_i ai 不一定递增,但小明又想在不修改程序的情况下正确运行样例,于是小明决定使用一种非常原始的解决方案:将所有数据划分成若干个数据段,段内数据编号连续,接着将同一段内的数据合并成新数据,其规模等于段内原数据的规模之和,小明将让新数据的规模能够递增。

也就是说,小明需要找到一些分界点 1 ≤ k 1 < k 2 < ⋯ < k p < n 1 \leq k_1 \lt k_2 \lt \cdots \lt k_p\lt n 1k1<k2<<kp<n
使得 ∑ i = 1 k 1 a i ≤ ∑ i = k 1 + 1 k 2 a i ≤ ⋯ ≤ ∑ i = k p + 1 n a i \sum_{i=1}^{k_1} a_i \leq \sum_{i=k_1+1}^{k_2} a_i \leq \cdots \leq \sum_{i=k_p+1}^{n} a_i i=1k1aii=k1+1k2aii=kp+1nai
注意 p p p 可以为 0 0 0 且此时 k 0 = 0 k_0 = 0 k0=0,也就是小明可以将所有数据合并在一起运行。

小明希望他的程序在正确运行样例情况下,运行时间也能尽量小,也就是最小化

( ∑ i = 1 k 1 a i ) 2 + ( ∑ i = k 1 + 1 k 2 a i ) 2 + ⋯ + ( ∑ i = k p + 1 n a i ) 2 (\sum_{i=1}^{k_1} a_i)^2 + (\sum_{i=k_1+1}^{k_2} a_i)^2 + \cdots + (\sum_{i=k_p+1}^{n} a_i)^2 (i=1k1ai)2+(i=k1+1k2ai)2++(i=kp+1nai)2

小明觉得这个问题非常有趣,并向你请教:给定 n n n a i a_i ai ,请你求出最优划分方案下,小明的程序的最小运行时间。

输入格式

由于本题的数据范围较大,部分测试点的 a i a_i ai将在程序内生成。

第一行两个整数 n , t y p e n, type n,type n n n 的意义见题目描述, t y p e type type 表示输入方式。

t y p e = 0 type = 0 type=0 ,则该测试点的 a i a_i ai 直接给出。输入文件接下来:第二行 n n n 个以空格分隔的整数 a i a_i ai,表示每组数据的规模。

t y p e = 1 type = 1 type=1 ,则该测试点的 a i a_i ai 将特殊生成,生成方式见后文。输入文件接下来:第二行六个以空格分隔的整数 x , y , z , b 1 , b 2 , m x, y, z, b_1, b_2, m x,y,z,b1,b2,m 。接下来 m m m 行中,第 i ( 1 ≤ i ≤ m ) i (1 \leq i \leq m) i(1im) 行包含三个以空格分隔的正整数 p i , l i , r i p_i, l_i, r_i pi,li,ri

对于 t y p e = 1 type = 1 type=1 的 23~25 号测试点, a i a_i ai 的生成方式如下:

给定整数 x , y , z , b 1 , b 2 , m x, y, z, b_1, b_2, m x,y,z,b1,b2,m ,以及 m m m 个三元组 ( p i , l i , r i ) (p_i, l_i, r_i) (pi,li,ri)

保证 n ≥ 2 n \geq 2 n2 。若 n > 2 n \gt 2 n>2,则 ∀ 3 ≤ i ≤ n , b i = ( x × b i − 1 + y × b i − 2 + z ) m o d    2 30 \forall 3 \leq i \leq n, b_i = (x \times b_{i−1} + y \times b_{i−2} + z) \mod 2^{30} 3in,bi=(x×bi1+y×bi2+z)mod230

保证 1 ≤ p i ≤ n , p m = n 1 \leq p_i \leq n, p_m = n 1pin,pm=n 。令 p 0 = 0 p_0 = 0 p0=0 ,则 p i p_i pi 还满足 ∀ 0 ≤ i < m \forall 0 \leq i \lt m 0i<m p i < p i + 1 p_i \lt p_{i+1} pi<pi+1

对于所有 1 ≤ j ≤ m 1 \leq j \leq m 1jm ,若下标值 i ( 1 ≤ i ≤ n ) i (1 \leq i \leq n) i(1in) 满足 p j − 1 < i ≤ p j p_{j−1} \lt i \leq p_j pj1<ipj ,则有
a i = ( b i m o d    ( r j − l j + 1 ) ) + l j a_i = \left(b_i \mod \left( r_j − l_j + 1 \right) \right) + l_j ai=(bimod(rjlj+1))+lj

上述数据生成方式仅是为了减少输入量大小,标准算法不依赖于该生成方式。

输出格式

输出一行一个整数,表示答案。

输入输出样例

输入 #1

5 0
5 1 7 9 9

输出 #1

247

输入 #2

10 0 
5 6 7 7 4 6 2 13 19 9

输出 #2

1256

输入 #3

10000000 1
123 456 789 12345 6789 3
2000000 123456789 987654321
7000000 234567891 876543219
10000000 456789123 567891234

输出 #3

4972194419293431240859891640

说明/提示

【样例 1 解释】

最优的划分方案为 { 5 , 1 } , { 7 } , { 9 } , { 9 } \{5,1\}, \{7\}, \{9\}, \{9\} {5,1},{7},{9},{9}。由 5 + 1 ≤ 7 ≤ 9 ≤ 9 5 + 1 \leq 7 \leq 9 \leq 9 5+1799 知该方案合法。

答案为 ( 5 + 1 ) 2 + 7 2 + 9 2 + 9 2 = 247 (5 + 1)^2 + 7^2 + 9^2 + 9^2 = 247 (5+1)2+72+92+92=247

虽然划分方案 { 5 } , { 1 } , { 7 } , { 9 } , { 9 } \{5\}, \{1\}, \{7\}, \{9\}, \{9\} {5},{1},{7},{9},{9} 对应的运行时间比 247 247 247 小,但它不是一组合法方案,因为 5 > 1 5 \gt 1 5>1

虽然划分方案 { 5 } , { 1 , 7 } , { 9 } , { 9 } \{5\}, \{1,7\}, \{9\}, \{9\} {5},{1,7},{9},{9} 合法,但该方案对应的运行时间为 251 251 251,比 247 247 247 大。

【样例 2 解释】

最优的划分方案为 { 5 } , { 6 } , { 7 } , { 7 } , { 4 , 6 , 2 } , { 13 } , { 19 , 9 } \{5\}, \{6\}, \{7\}, \{7\}, \{4,6,2\}, \{13\}, \{19,9\} {5},{6},{7},{7},{4,6,2},{13},{19,9}

【数据范围】

在这里插入图片描述

对于 t y p e = 0 type=0 type=0 的所有测试点,保证最后输出的答案 ≤ 4 × 1 0 18 \leq 4 \times 10^{18} 4×1018

所有测试点满足: t y p e ∈ { 0 , 1 } type \in \{0,1\} type{0,1} 2 ≤ n ≤ 4 × 1 0 7 2 \leq n \leq 4 \times 10^7 2n4×107 1 ≤ a i ≤ 1 0 9 1 \leq a_i \leq 10^9 1ai109 1 ≤ m ≤ 1 0 5 1 \leq m \leq 10^5 1m105 1 ≤ l i ≤ r i ≤ 1 0 9 1 \leq l_i \leq r_i \leq 10^9 1liri109 0 ≤ x , y , z , b 1 , b 2 < 2 30 0 \leq x,y,z,b_1,b_2 \lt 2^{30} 0x,y,z,b1,b2<230

分析

这道题很明显告诉我们需要在给定的长度为 n n n 的数组中,划分出任意段连续不重合的元素,并使每一段元素之和大于等于前一段元素之和,最后对每一段元素和分别平方,再求和。

60pts

对于前 60 % 60\% 60% 的数据,我们可以看出 O ( n 2 ) O(n^2) On2的算法可以通过此题,那么开始思考这一部分分的算法。

我们用 d p [ i ] ( 1 ≤ i ≤ n ) dp[i] (1\leq i \leq n) dp[i](1in) 来表示从1号元素一直到第i号元素的最优解,不论划分成几段。再用 a [ i ] a[i] a[i] 表示从 1 1 1 i i i 的元素前缀和。

那么 d p [ 1 ] dp[1] dp[1] 一定等于 a [ 1 ] × a [ 1 ] a[1] \times a[1] a[1]×a[1]
如果 d p [ i ] ( i > 1 ) dp[i] (i \gt 1) dp[i](i>1) 是最优解的话,那么一定存在从某一个 d p [ j ] ( 1 ≤ j < i ) dp[j] (1 \leq j \lt i) dp[j](1j<i) 转移过来,
也就是 d p [ i ] = d p [ j ] + ( a [ i ] − a [ j ] ) 2 dp[i] = dp[j] + (a[i] - a[j])^2 dp[i]=dp[j]+(a[i]a[j])2
那么60分的做法就有了,从 1 1 1 循环至 n n n(变量为 i i i ,意为计算至 1 1 1 i i i 的最优解),再在每一层循环中再从 0 0 0 循环至 i − 1 i-1 i1,(变量为 j j j ,意为对于小于 i i i 的每一个 j j j ,暴力判断最优解)

60分代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,type;
long long a[500005],cnt,dp[500005],lastt[500005],ans,maxx=9223372036854775807,temp;
int main(){
//  freopen("partition.in","r",stdin);
//  freopen("partition.out","w",stdout);
    scanf("%d%d",&n,&type);
    if(type==1){ //不管23~25样例点
        printf("0\n");
        return 0;
    }
    for(register int i=1;i<=n;++i){
        scanf("%lld",&a[i]);
    }
    for(register int i=1;i<=n;++i)dp[i]=maxx;  //初始化
    dp[1]=a[1]*a[1],lastt[1]=a[1]*a[1]; //初始化
    for(register int i=1;i<=n;++i)a[i]+=a[i-1]; //前缀和
    for(register int i=2;i<=n;++i){
        for(register int j=0;j<i;++j){
            temp=(a[i]-a[j])*(a[i]-a[j]);     //差值
            if(temp>=lastt[j]){  //当前段之和大于上一段之和
                if(dp[i]>dp[j]+temp){ //转移
                    dp[i]=dp[j]+temp,lastt[i]=temp; //更新
                }
            }
        }
    }
    printf("%lld\n",dp[n]);
    return 0;
}

3.树的重心

题目描述

小简单正在学习离散数学,今天的内容是图论基础,在课上他做了如下两条笔记:

一个大小为 n n n 的树由 n n n 个结点与 n − 1 n − 1 n1 条无向边构成,且满足任意两个结点间有且仅有一条简单路径。在树中删去一个结点及与它关联的边,树将分裂为若干个子树;而在树中删去一条边(保留关联结点,下同),树将分裂为恰好两个子树。

对于一个大小为 n n n 的树与任意一个树中结点 c c c ,称 c c c 是该树的重心当且仅当在树中删去 c c c 及与它关联的边后,分裂出的所有子树的大小均不超过 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor 2n(其中 ⌊ x ⌋ \lfloor x \rfloor x 是下取整函数)。对于包含至少一个结点的树,它的重心只可能有 1 1 1 2 2 2 个。

课后老师给出了一个大小为 n n n 的树 S S S,树中结点从 1 ∼ n 1 \sim n 1n 编号。小简单的课后作业是求出 S S S 单独删去每条边后,分裂出的两个子树的重心编号和之和。即:

∑ ( u , v ) ∈ E \sum_{(u,v) \in E} (u,v)E ( ∑ 1 ≤ x ≤ n 且 x 号 点 是 S u ′ 的 重 心 x + ( \sum_{1 \leq x \leq n \atop 且 x 号点是 S'_u 的重心}x + (xSu1xnx+ ∑ 1 ≤ y ≤ n 且 y 号 点 是 S v ′ 的 重 心 y ) \sum_{1 \leq y \leq n \atop 且 y 号点是 S'_v 的重心}y ) ySv1yny)

上式中, E E E 表示树 S S S 的边集, ( u , v ) (u,v) (u,v) 表示一条连接 u u u 号点和 v v v 号点的边。 S u ′ S'_u Su S v ′ S'_v Sv
分别表示树 S S S 删去边 ( u , v ) (u,v) (u,v) 后, u u u 号点与 v v v 号点所在的被分裂出的子树。

小简单觉得作业并不简单,只好向你求助,请你教教他。

输入格式

本题包含多组测试数据

第一行一个整数 T T T 表示数据组数。

接下来依次给出每组输入数据,对于每组数据:

第一行一个整数 n n n 表示树 S S S 的大小。

接下来 n − 1 n − 1 n1 行,每行两个以空格分隔的整数 u i , v i u_i ,v_i uivi,表示树中的一条边 ( u i , v i ) (u_i,v_i) (ui,vi)

输出格式

T T T 行,每行一个整数,第 i i i 行的整数表示:第 i i i 组数据给出的树单独删去每条边后,分裂出的两个子树的重心编号和之和。

输入输出样例

输入 #1

2
5
1 2
2 3
2 4
3 5
7
1 2
1 3
1 4
3 5
3 6
6 7

输出 #1

32
56

说明/提示

【样例 1 解释】

对于第一组数据:

删去边 ( 1 , 2 ) (1,2) (1,2),1 号点所在子树重心编号为 { 1 } \{1\} {1},2 号点所在子树重心编号为 { 2 , 3 } \{2,3\} {2,3}

删去边 ( 2 , 3 ) (2,3) (2,3),2 号点所在子树重心编号为 { 2 } \{2\} {2},3 号点所在子树重心编号为 { 3 , 5 } \{3,5\} {3,5}

删去边 ( 2 , 4 ) (2,4) (2,4),2 号点所在子树重心编号为 { 2 , 3 } \{2,3\} {2,3},4 号点所在子树重心编号为 { 4 } \{4\} {4}

删去边 ( 3 , 5 ) (3,5) (3,5),3 号点所在子树重心编号为 { 2 } \{2\} {2},5 号点所在子树重心编号为 { 5 } \{5\} {5}

因此答案为 1 + 2 + 3 + 2 + 3 + 5 + 2 + 3 + 4 + 2 + 5 = 32 1 + 2 + 3 + 2 + 3 + 5 + 2 + 3 + 4 + 2 + 5 = 32 1+2+3+2+3+5+2+3+4+2+5=32

【数据范围】

在这里插入图片描述

表中特殊性质一栏,两个变量的含义为存在一个 1 ∼ n 1 \sim n 1n 的排列 p i ( 1 ≤ i ≤ n ) p_i (1 \leq i \leq n) pi(1in),使得:

  • A:树的形态是一条链。即 ∀ 1 ≤ i < n \forall 1 \leq i \lt n 1i<n ,存在一条边 ( p i , p i + 1 ) (p_i, p_{i + 1}) (pi,pi+1)
  • B:树的形态是一个完美二叉树。即 ∀ 1 ≤ i ≤ n − 1 2 \forall 1 \leq i \leq \frac{n-1}{2} 1i2n1,存在两条边 ( p i , p 2 i ) (p_i, p_{2i}) (pi,p2i) ( p i , p 2 i + 1 ) (p_i, p_{2i+1}) (pi,p2i+1)

对于所有测试点: 1 ≤ T ≤ 5 1 \leq T \leq 5 1T5 , 1 ≤ u i , v i ≤ n 1 \leq u_i,v_i \leq n 1ui,vin 。保证给出的图是一个树。

分析

40pts

可以想到 O ( n 2 ) O(n^2) O(n2) 每次暴力找重心。

通过分析样例可以得到一些性质,这些性质可能有利于问题的求解:

  1. 一棵树如果有两个重心,这两个重心一定是相邻的
  2. 一棵树的重心一定在根节点所在的重链上
  3. 一棵树的重心一定是以该树根节点重儿子为根的子树的重心的祖先

根据性质1,我们可以先找到深度较大的重心,然后对于已求出的重心再判断其父亲是否也是重心(因此接下来说的重心都是深度较大的重心);
根据性质2,我们在找重心的时候可以只往根节点所在的重链上找;
根据性质3,我们可以从下往上找重心,不用每次重新找。

具体步骤

  1. 找重心
    dfs一遍随便找到一个重心即可。把整棵树的重心作为之后的根节点。
  2. 预处理节点信息
    预处理每个节点的子树大小、重儿子、深度、父亲以及属于根节点哪个儿子的子树,同时找到根节点的次重儿子。dfs一遍即可。
  3. 预处理答案
    分别预处理出根节点重儿子所在的重链的答案、根节点次重儿子所在的重链的答案以及以每个节点为根的子树的重心。每种情况从下到上走一遍即可。
  4. 枚举删边求答案
    实际上枚举删掉子树也是可以的,这里还是用删边来写。

完整代码

#include<bits/stdc++.h> //40分暴力 
#define ll long long
#define ano ((i-1)^1)+1
using namespace std;
const int N=3e5+100;
int T,n,cnt,tot;
ll ans;
int head[N],ver[2*N],Next[2*N];
int maxson[N],siz[N],d[N],nowh[N],nowsiz[N],ffa[N];
bool sp[N],v[N];
void add(int x,int y){
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void dfs(int x,int root){
    if(x!=root) nowsiz[x]=1;
    v[x]=1;
    for(int i=head[x];i;i=Next[i])
        if(!sp[ver[i]] && !v[ver[i]]){
            int y=ver[i];
            dfs(y,root);
            maxson[x]=max(maxson[x],nowsiz[y]);
            if(x!=root)
                nowsiz[x]+=nowsiz[y];
        }
    if(maxson[x]*2<=nowsiz[root] && (nowsiz[root]-nowsiz[x])*2<=nowsiz[root])
        nowh[++cnt]=x;
}//找重心
void pre(int x,int f,int root){
    siz[x]=1,d[x]=d[f]+1;
    for(int i=head[x];i;i=Next[i])
        if(!d[ver[i]] && ver[i]){
            int y=ver[i];
            if(x==root) ffa[y]=y;
            else ffa[y]=ffa[f];
            pre(y,x,root);
            siz[x]+=siz[y];
        }
}        //预处理子树大小和深度
void clearly(){
    memset(maxson,0,sizeof(maxson));
    memset(nowsiz,0,sizeof(nowsiz));
    memset(v,0,sizeof(v));
    cnt=0;
}
void solve(){
    pre(1,0,1);
    for(int i=1;i<=tot;i+=2){
        clearly();
        int x=ver[i],y=ver[ano];
        sp[x]=sp[y]=1;//dfs时不走x,y
        if(d[x]>d[y]) swap(x,y);//令y为深度较大的节点
        nowsiz[x]=n-siz[y],nowsiz[y]=siz[y];
        dfs(x,x),dfs(y,y);
        sp[x]=sp[y]=0;//还原
        for(int j=1;j<=cnt;j++)
            ans+=nowh[j];
    }
}
void clear(){
    memset(head,0,sizeof(head));
    memset(Next,0,sizeof(Next));
    memset(siz,0,sizeof(siz));
    memset(d,0,sizeof(d));
    tot=ans=0;
}
int main(){
    scanf("%d",&T);
    while(T--){
        clear();
        scanf("%d",&n);
        for(int i=1,x,y;i<n;i++){
            scanf("%d%d",&x,&y);
            add(x,y);
        }
        solve();
        printf("%lld\n",ans);
    }
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
2010年全国青少年信息学奥林匹克联赛(NOIP)提高的复赛是一次重要的比赛。在这次比赛中,参赛选手需要展示出在信息学方面的专业知识和技能。 比赛的复赛共有两个题目,分别是算法设计题和程序设计题。算法设计题要求选手设计一个高效的算法来解决特定的问题,参赛选手需要对问题进行深入分析,找到最佳的解决方案。程序设计题要求选手根据题目要求,使用编程语言编写出能够正确解决问题的程序。 在比赛中,选手们需要在限定的时间内完成这两个题目。时间限制会对选手的应试能力和抗压能力提出要求。选手需要利用自己所学的知识和经验,灵活地运用在实际的问题中,制定出最佳的解决方案。 在复赛中,评委们会根据选手的算法设计和程序设计的质量来进行评判。他们会考察选手的算法思路是否清晰,是否能通过代码来实现自己的想法。评委们还会参考选手的编码风格,包括代码的可读性和规范性。这些综合因素将影响选手在比赛中的表现。 参加2010年NOIP提高的复赛对于选手们来说是一次难得的机会。这次比赛既能测试选手们的能力和水平,也能提供一个展示自己的舞台。在这个比赛中,选手们需要充分发挥自己的优势,努力争取好的成绩,并从中积累经验,提高自己的编程能力和解决问题的能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值