理想正方形(单调队列),22数(数位dp)

1.洛谷[HAOI2007]理想的正方形 (单调队列)

原题链接

题目描述

  有一个 a × b a \times b a×b 的整数组成的矩阵,现请你从中找出一个 n × n n \times n n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。

  输入格式

  第一行为 3 3 3 个整数,分别表示 a , b , n a,b,n a,b,n 的值。

  第二行至第 a + 1 a+1 a+1 行每行为 b b b 个非负整数,表示矩阵中相应位置上的数。每行相邻两数之间用一空格分隔。

  输出格式

  仅一个整数,为 a × b a \times b a×b 矩阵中所有“ n × n n \times n n×n 正方形区域中的最大整数和最小整数的差值”的最小值。

  样例

  样例输入

  5 4 2

  1 2 5 6

  0 17 16 0

  16 17 2 1

  2 10 2 1

  1 2 2 2

  样例输出

  1

  提示

  问题规模。

  矩阵中的所有数都不超过 1 , 000 , 000 , 000 1,000,000,000 1,000,000,000

   20 % 20\% 20% 的数据 2 ≤ a , b ≤ 100 , n ≤ a , n ≤ b , n ≤ 10 2 \le a,b \le 100,n \le a,n \le b,n \le 10 2a,b100,na,nb,n10

   100 % 100\% 100% 的数据 2 ≤ a , b ≤ 1000 , n ≤ a , n ≤ b , n ≤ 100 2 \le a,b \le 1000,n \le a,n \le b,n \le 100 2a,b1000,na,nb,n100


  滑动窗口求最大最小值,对于每一行吧每个长度为n的区间的最值求出,此时可以看作每个长度为n的区间内的最值位于该区间最后一个位置,然后再在列上对这些最值求一次最大最小得到该n*n区间内正方形最值,最后维护ans=min(ans,b[i]-c[i])。

#include <iostream>
#include <cstdio>
using namespace std;
const int maxn=1010;
int n,m,k;
int row_max[maxn][maxn],row_min[maxn][maxn];//行的最大最小值
int q[maxn],w[maxn][maxn];//单调队列,矩阵
void get_min(int a[],int b[],int len){
    int hh=0,tt=-1;
    for(int i=1;i<=len;++i){
        if(hh<=tt&&i-k>=q[hh]) ++hh;
        while(hh<=tt&&a[q[tt]]>=a[i]) --tt;
        q[++tt]=i;
        b[i]=a[q[hh]];
    }
    //求最小值维护递增单调递增的单调队列
    //可用区间为从i开始往前的k个元素即[i-k+1,i]
}
void get_max(int a[],int b[],int len){
    int hh=0,tt=-1;
    for(int i=1;i<=len;++i){
        if(hh<=tt&&i-k>=q[hh]) ++hh;
        while(hh<=tt&&a[q[tt]]<=a[i]) --tt;
        q[++tt]=i;
        b[i]=a[q[hh]];
    }
    //求最大值维护单调递减的单调队列
    //可用区间为从i开始往前的k个元素即[i-k+1,i]
}

int main()
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            scanf("%d",&w[i][j]);
        }
    }
    for(int i=1;i<=n;++i){
        get_min(w[i],row_min[i],m);
        get_max(w[i],row_max[i],m);
    }
    int a[maxn],b[maxn],c[maxn];
    int ans=0x3f3f3f3f;
    for(int i=k;i<=m;++i){
        for(int j=1;j<=n;++j) a[j]=row_max[j][i];
        get_max(a,b,n);
        for(int j=1;j<=n;++j) a[j]=row_min[j][i];
        get_min(a,c,n);
        //列元素不连续先吧每列元素放到临时数组a中,b中维护最大值,c中维护最小值
        for(int j=k;j<=n;++j){
            ans=min(ans,b[j]-c[j]);
        }//从[k,k]开始往后枚举所有的最值之差并求最小
    }
    printf("%d\n",ans);
    return 0;
}

2.牛客网.22数(数位dp)

原题链接


  题目描述

  现在定义一种22数, 这种数各个数位上的数字之和等于其位数的二倍

  求 1 ~ n 中,22数的个数 x。

  输入描述:

  输入包含一行一个正整数 n ( 1 ≤ n ≤ 1 0 10 ) n(1 \leq n \leq 10^{10}) n(1n1010)

  输出描述:

  输出包含一行一个数表示 1 ∼ n 1 \sim n 1n范围内22数的个数 x。

   示例

  输入 :22

  输出: 3

  说明

  1~22范围内包含3个22数分别是2,13,22。


   dp[i]j][k] 表示数位为i,最高位为j,数位和为k的所有方案数。先预处理出所有的dp数组, 1 0 10 10^{10} 1010最多有十个数位,所以k理论上最多为20,保险起见可以多开,同时爆了int,本题所有数组均需要开long long

   dp的预处理考虑数位为i的时候最高位为j,总和为k,他会从dp[i-1][t][k-j]转移过来(k>j, 0<=t<=9)

void init(){
    for(int i=0;i<=9;++i){
        f[1][i][i]++;
    }
    for(int i=1;i<=10;++i){// n
        for(int j=0;j<=9;++j){// nmax
            for(int k=0;k<=20;++k){ //sum
                for(int t=0;t<=9;++t) //i-1 
                if(k>=j){
                    f[i][j][k]+=f[i-1][t][k-j];
                }
            }
        }
    }
}


   对于之后的方案数,枚举所有数位,先将其各位拆分出来,然后对于每个数位上数字x有两种可能:

   第一:该位选取0-x-1范围内的数字,那么ans+=dp[i+1][j][n*2-last](从i到0有i+1位数字,除最高位外0<=j<x,last为之前所有位的和。第二:该位选取

   第二:该位选取x,那么last+=x,当i==0时,如果n * 2 == last,++ans


   该过程中如果出现了last>=n*2的情况可以直接break


   这是所有不含前导0的情况,最后再额外处理除前导0的情况即可,从第1位到第n-1位,每一位枚举1-9, ans+=dp[i][j][i*2]

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[15][15][100];//i->n j->n-max  k ->sum
ll n;
void init(){
    for(int i=0;i<=9;++i){
        f[1][i][i]++;
    }
    for(int i=1;i<=10;++i){// n
        for(int j=0;j<=9;++j){// nmax
            for(int k=0;k<=20;++k){ //sum
                for(int t=0;t<=9;++t) //i-1 
                if(k>=j){
                    f[i][j][k]+=f[i-1][t][k-j];
                }
            }
        }
    }
}
ll dp(ll x){
    vector<ll> tmp;
    while(x) tmp.push_back(x%10),x/=10;
    ll n=tmp.size();
    ll ans=0;
    ll last=0;
    for(int i=tmp.size()-1;i>=0;--i){
        for(int j=i==tmp.size()-1;j<tmp[i];++j){
            ans+=f[i+1][j][2*n-last];
        }
        last+=tmp[i];
        if(last>2*n) break;
        if(i==0&&last==n*2) ans++;
    }
    for(int i=1;i<n;++i){
        for(int j=1;j<=9;++j){
            ans+=f[i][j][i*2];
        }
    }
    return ans;
}

int main()
{
    init();
    cin>>n;
    cout<<dp(n);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值