【题解报告】ZJNU综合训练(2021.1.26)

【题解报告】ZJNU综合训练(2021.1.26)

B

  • n n n 盒糖果并排放,每盒有数量 r i r_i ri ,里面颜色为 c i c_i ci
  • 如果吃糖,必须在该位置的糖全部吃完,且必须比上一次吃得数量多,且必须与上一次吃的糖的颜色不一样。
  • 每次向相邻方向走一格的时间花费为 1 1 1,吃糖不花费时间。
    起点在 s s s,问你至少吃 k k k 颗糖最少花费时间为多少。

  • 题目要求真地多,而且我开始还读错题意了。。
    d p [ i ] [ j ] dp[i][j] dp[i][j] 表示你现在在位置 i i i 且吃了该位置的糖,花了 j j j 时间的最多吃糖数。
    为什么可以这么设呢?因为你每次吃糖一定是吃完的,你中间如果走过去没有吃得话,其实是为了走到目的地然后吃,所以设的不是你目前的位置,而是你目前吃完糖的位置
  • 转移条件也很简单,满足糖果颜色不同、数量要更多就行了。
    注意,时间上限是多少? n 2 n^2 n2,因为你不可能没吃糖,经过同一个点两次。

时间复杂度: O ( n 4 log ⁡ n ) O(n^4\log n) O(n4logn)

const int MAX = 55;
int n,s,k;
int r[MAX];
string c;
int dp[MAX][MAX*MAX];

int main()
{
    
    
    cin >> n >> s >> k;
    for(int i = 1;i <= n;++i)cin >> r[i];
    cin >> c;
    c = " " + c;
    memset(dp,-1,sizeof(dp));
    priority_queue<pair<int,int> >Q;
    for(int i = 1;i <= n;++i){
    
    
        dp[i][abs(i-s)] = r[i];
        Q.push(make_pair(-(abs(i-s)),i));
    }
    while(!Q.empty()){
    
    
        int d = -Q.top().first;
        int x = Q.top().second;
        Q.pop();
        if(dp[x][d] >= k){
    
    
            cout << d;
            return 0;
        }
        for(int i = 1;i <= n;++i){
    
    
            if(c[x] != c[i] && r[i] > r[x] && dp[i][abs(i-x)+d] < dp[x][d] + r[i]){
    
    
                dp[i][abs(i-x)+d] = dp[x][d] + r[i];
                Q.push(make_pair(-(abs(i-x)+d),i));
            }
        }
    }
    cout << -1;
    return 0;
}

C

  • 给你一个长度为 1 0 5 10^5 105 的数字串。
    问你,中间连续删掉一段数字之后,剩下的数字头尾相接,形成删数。问你所有不同的删法后,所拼接出来的删数的和取模 998244353 998244353 998244353 是多少。
  • 比如 107 107 107,删掉中间连续一段之后会变成 07 、 17 、 10 、 7 、 1 、 0 07 、 17、 10、 7、 1、 0 071710710,他们的和为 42 42 42

  • 把数字拼接起来之后再算怎么搞都是搞不出来的、、我们按头和尾来考虑。
    如果他们俩能连接起来,我们给他们连一条线。下面的 + + +号表示把他们收尾拼接。
    比如 ϕ + 07 = 07 \phi +07=07 ϕ+07=07 ϕ + ϕ = 0 \phi+\phi=0 ϕ+ϕ=0 1 + 7 = 17 1+7=17 1+7=17
    在这里插入图片描述
  • 考虑每一串头和每一串尾到底对答案作出多少贡献。
    注意:头的拼接还要考虑拼接后相当于头的数字增加了 1 0 k 10^k 10k
  • 尾的收益:最长的尾只贡献 1 1 1次,次长的贡献 2 2 2次等
    尾的收益比较好算,单个串的数字好算,单个串的贡献次数也好算。
  • 头的收益:
    (1) ϕ \phi ϕ 的贡献为 1 1 1 次和长度为 2 2 2 的串拼接, 1 1 1 次为和长度为 1 1 1 的串拼接, 1 1 1 次和长度为 0 0 0 的串拼接,它的贡献就是 0 × ( 1 0 2 + 1 0 1 + 1 0 0 ) 0\times(10^2+10^1+10^0) 0×(102+101+100)
    (2) 1 1 1 的贡献为 1 1 1 次和长度为 1 1 1 的串拼接, 1 1 1 次和长度为 0 0 0 的串拼接,它的收益就是 1 × ( 1 0 1 + 1 0 0 ) 1\times(10^1+10^0) 1×(101+100)
    (3)对于某一个头串,它的贡献为 它 本 身 × p r e [ x ] 它本身\times pre[x] ×pre[x] ,其中 p r e [ x ] = 1 0 x + 1 0 x − 1 + ⋯ + 1 0 0 pre[x]=10^x+10^{x-1}+\cdots+10^0 pre[x]=10x+10x1++100。这个 x x x 取原串长减去该头串长再减一。

时间复杂度: O ( l o g 10 S ) O(log_{10}S) O(log10S)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
char ss[MAX];
ll pre[MAX];
void init(int x){
    
    
    pre[0] = 1;
    ll base = 10;
    for(int i = 1;i <= x;++i){
    
    
        pre[i] = (pre[i-1] + base) % MOD;
        base = base * 10 % MOD;
    }
}
int main()
{
    
    
    init((int)1e5+50);
    scanf("%s",ss);
    int len = strlen(ss);
    ll sum = 0;
    ll base = 1;
    ll tmp = 0;
    for(int i = len-1;i >= 1;--i){
    
    
        tmp = (base * (ss[i]-'0') % MOD + tmp) % MOD;
        sum = (sum + tmp * i % MOD) % MOD;
        base = (base * 10) % MOD;
    }
    tmp = 0;
    base = 1;
    for(int i = 0;i < len-1;++i){
    
    
        tmp = (tmp * 10 % MOD + (ss[i]-'0')) % MOD;
        sum = (sum + tmp * pre[len-i-2] % MOD) % MOD;
        base = (base * 10) % MOD;
    }
    printf("%lld",sum);
    return 0;
}

D

  • 问你对于一个序列,是否可以删掉一个数字后,其他数字成等差,问你删掉哪一个。
  • 序列先排序,再考虑删头或者删尾或者删中间。
    删中间的话:要删除的那个数字的序号一定是 该位置之后的数字减去该位置之前的数字的和最小
    考虑一个等差 5 、 10 、 15 、 20 、 25 5、 10、 15、 20、 25 510152025
    如果要删中间,你多余的数字应该要放在这些数字的中间
    5 、 10 、 15 、 18 、 20 、 25 5、 10、 15、 18、 20、 25 51015182025
    此时明显 20 − 18 + 18 − 15 = 20 − 15 = 5 20-18+18-15=20-15=5 2018+1815=2015=5 是所有里面最小的,因为其他的数字 15 − 5 = 10 15-5=10 155=10 明显是两倍等差。

F

  • 给你一个树,但是树的边是单向的。
    你要选择首都的位置,要修改一些道路的朝向,使得首都能够到达所有的位置。
    问你最少修改多少条路,以及首都的所有可选位置。
  • 树形 D P DP DP。设 d p [ x ] dp[x] dp[x] 表示在 x x x 位置处选择首都的话,需要改多少条路。
    虽然这个 d p [ x ] dp[x] dp[x] 我们是直接算不出来的,但是我们知道相邻两个位置的 d p dp dp 值的大小关系
    在这里插入图片描述
  • 这样就能算出哪个节点的 d p dp dp 值相对最小,然后以该节点作为起点进行 d f s dfs dfs,查看需要修改几条边就行了。
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
vector<pair<int,int> >V[MAX];
int dp[MAX];
int mn,id;
void dfs(int u,int fa){
    
    		/// 跑dp
    if(dp[u] < mn){
    
    
        mn = dp[u];
        id = u;
    }
    for(auto it : V[u]){
    
    
        int v = it.first;
        int w = it.second;
        if(v == fa)continue;
        if(w == 1)dp[v] = dp[u] + 1;
        else dp[v] = dp[u] - 1;
        dfs(v,u);
    }
}
int ans;
void dfs2(int u,int fa){
    
    	// 跑最小修改路径数量
    for(auto it : V[u]){
    
    
        int v = it.first;
        int w = it.second;
        if(v == fa)continue;
        if(w == -1)ans++;
        dfs2(v,u);
    }
}
int main()
{
    
    
    int n;scanf("%d",&n);
    for(int i = 1;i < n;++i){
    
    
        int ta,tb;scanf("%d%d",&ta,&tb);
        V[ta].push_back(make_pair(tb,1));
        V[tb].push_back(make_pair(ta,-1));
    }
    mn = 0;id = 1;
    dfs(1,1);
    dfs2(id,id);
    printf("%d\n",ans);
    for(int i = 1;i <= n;++i){
    
    
        if(dp[i] == dp[id])cout << i << " ";
    }
    return 0;
}

H

  • 问你,最少正几边形,你通过顶点连接能够得到 d d d 的角度(角度制),该角度为 1 ∼ 180 1\sim180 1180的整数。
    在这里插入图片描述
  • 圆周角所对的弧长相等的话,圆周角是相同的。因此一个正 n n n 边形,算出一份圆周角是多少度,然后每次暴力枚举份数 1 ∼ n − 2 1\sim n-2 1n2,查看是不是整数度数即可。
int mn[MAX];

int main()
{
    
    

    int T;cin >> T;
    while(T--){
    
    
        int n;cin >> n;
        int mn = INF;
        for(int i = 3;i <= 700;++i){
    
    
            double deg = (180.0*i-360)/(1.0*i)/(1.0*i-2);
            for(int j = 1;j <= i-2;++j){
    
    
                double de = deg * j;
                if(fabs(de-n)<EPS)mn=min(mn,i);
            }
        }
        if(mn == INF)cout << -1 << endl;
        else cout << mn << endl;
    }
    return 0;
}

I

  • m e r g e ( A , B ) merge(A,B) merge(A,B) 函数表示每次取 A 、 B A、B AB 序列的开头较小的元素,然后把该位置的元素拿出来,放在集合的尾。如果有一个序列是空的话,直接取非空序列。
    比如 m e r g e ( { 3 , 2 } , { 1 , 4 } ) = { 1 , 3 , 2 , 4 } merge(\{3,2\},\{1,4\})=\{1,3,2,4\} merge({ 3,2},{ 1,4})={ 1,3,2,4}
    给定一个 2 n 2n 2n 个元素的全排列。问该全排列是否能通过 m e r g e ( A , B ) merge(A,B) merge(A,B),且 ∣ A ∣ = ∣ B ∣ = n |A|=|B|=n A=B=n得到。

  • 非常巧妙的一题。
    (1)对于目前的最大元素 2 n 2n 2n ,假设该元素原来是在 A A A 里面,那么该位置之后的所有数字一定全属于 A A A 序列。然后我们把这些数字给拿出来。
    (2)对于剩下的数字,有最大元素 p p p ,该位置到末尾的位置一定全属于 A A A B B B 序列。
    (3)重复上述步骤,我们就得到了一段一段的序列,每一段必须都属于 A A A 或者 B B B 序列。
    (4)怎么求是否能存在 ∣ A ∣ = ∣ B ∣ = n |A|=|B|=n A=B=n 呢?设一个 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示目前处理到第 i i i 段,若 d p [ i ] [ j ] = 1 dp[i][j]=1 dp[i][j]=1 则表示目前我们能拿到长度为 j j j 的段。最后看 d p [ 段 数 ] [ n ] = 1   o r   0 dp[段数][n]=1\ or\ 0 dp[][n]=1 or 0就可以了。
    时间复杂度: O ( n 2 ) O(n^2) O(n2)
int aa[MAX];
int pos[MAX];
bool use[MAX];
int shu[MAX];
int dp[MAX][MAX];
int main()
{
    
    
    int T;cin >> T;
    while(T--){
    
    
        int n;cin >> n;
        int ed = n * 2;
        for(int i = 1;i <= ed;++i){
    
    
            cin >> aa[i];
            pos[aa[i]] = i;
            use[aa[i]] = 0;
        }
        int you = ed;
        int last = ed;
        int cnt = 0;
        while(you){
    
    
            while(use[last])last--;
            for(int i = pos[last];i <= you;++i)use[aa[i]] = 1;
            shu[++cnt] = you - pos[last] + 1;
            you = pos[last] - 1;
        }
        for(int i = 1;i <= cnt;++i)
        for(int j = 0;j <= ed;++j)
            dp[i][j] = 0;
        dp[0][0] = 1;
        for(int i = 1;i <= cnt;++i){
    
    
            for(int j = 0;j <= ed;++j){
    
    
                if(j >= shu[i])dp[i][j] |= dp[i-1][j-shu[i]];
                dp[i][j] |= dp[i-1][j];		/// 这个不要忘记转移
            }
        }
        if(dp[cnt][n])puts("YES");
        else puts("NO");
    }
    return 0;
}

J

  • n + 1 n+1 n+1个点,第 0 0 0个点的位置为 0 0 0,第 i i i 个点的位置为 i i i
    对于中间 1 ∼ n 1\sim n 1n的点,每个点都有 1 2 \frac{1}{2} 21 的概率放置一个信号塔,该点为 i i i ,信号塔的强度如果为 p p p ,则他能覆盖到 j j j 点的条件是 ∣ i − j ∣ < p |i-j|<p ij<p
    所有塔的信号强度是你自己调整的,可以都不同。
    求:有多少的概率使得你可以调整所有信号塔,使得 1 ∼ n 1\sim n 1n的所有点都只被一个信号塔的信号覆盖,且 0 0 0 n + 1 n+1 n+1 没有被信号覆盖

  • 可以想到,不管信号强度为多少,该信号塔能覆盖到的数量一定是奇数
    题目就转变为 d p ( i ) 2 n \frac{dp(i)}{2^n} 2ndp(i),其中 d p ( i ) dp(i) dp(i) 表示 i i i 能被拆成奇数的方案个数。(注意这里是顺序可换的方案)
    比如 d p ( 3 ) = 2 dp(3)=2 dp(3)=2,因为 3 = 1 + 1 + 1 = 3 3=1+1+1=3 3=1+1+1=3
    考虑状态转移方程就可以了:
    d p ( i ) = d p ( i − 1 ) + d p ( i − 3 ) + ⋯ + d p ( ( x − ( 2 k + 1 ) ) > 0 ) dp(i)=dp(i-1)+dp(i-3)+\cdots+dp((x-(2k+1))>0) dp(i)=dp(i1)+dp(i3)++dp((x(2k+1))>0)
    就用一个前缀和 p r e [ x ] = d p ( x ) + d p ( x − 2 ) + ⋯ pre[x]=dp(x)+dp(x-2)+\cdots pre[x]=dp(x)+dp(x2)+ 就可以了。

时间复杂度: O ( N ) O(N) O(N)

/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
ll dp[MAX];
ll pre[MAX];        /// pre[i] = dp[i] + dp[i-2] + dp[i-4]...
int main()
{
    
    
    int n;cin >> n;
    dp[0] = 1;
    pre[0] = 1;
    dp[1] = 1;
    pre[1] = 1;
    for(int i = 2;i <= n;++i){
    
    
        dp[i] = pre[i-1];
        pre[i] = (pre[i-2] + dp[i]) % MOD;
    }
    cout << dp[n] * inv(qpow(2,n)) % MOD;
    return 0;
}

M

  • 一张 n × m n\times m n×m的桌子,有无穷多的盘子,盘子半径都为 r r r
    两个人轮流放盘子,盘子不能重叠,不能边超出桌子。
    问你先手必胜还是后手必胜

  • 这题真不是水题也不是 B U G BUG BUG 题呀。。
    如果先手第一个盘子都放不下,那肯定是后手赢了。
    否则,先手第一个盘子放在该桌子的正中心处
    这样,构造除了一个中心对称图形。后手不论怎么放,先手都放在该位置的中心对称位置处。这样,不论后手怎么放,先手都能有位置放(易证),因为每一次先手放置的时候该图都是中心对称图形。

猜你喜欢

转载自blog.csdn.net/weixin_45775438/article/details/113189901