题解:动态规划


目录
  • 题目:
  • 解析
    • 部分分
      • 部分分代码(60分)
    • 全部分
      • AC代码

题目:

解析

部分分

这道题首先不考虑优化,我们该怎么写呢?显然,这是一个区间dp,我们可以利用记忆化搜索来写。
1、定义状态:dp[i][j]表示前i头分j段的最小价值。
2、状态转移方程:ans = min(ans,value(x,i) + dfs_dp(i + 1,left - 1));
这个状态转移方程和https://www.cnblogs.com/Crazyman-W/p/14872804.html 这道题十分类似。
3、然后是计算有多少对羊:

int value(int x,int y) {
    int ans = 0;
    memset(sum,0,sizeof(sum));
    int mn = 2e9,mx = 0;
    for(int i = x;i <= y; i++) {
	    sum[a[i]]++;
	    mx = max(mx,a[i]);
	    mn = min(mn,a[i]);
    }
    for(int i = mn;i <= mx; i++) {
	    ans += (sum[a[i]] * (sum[a[i]] - 1) ) / 2; //用排列组合算出每个数有多少对
	    sum[a[i]] = 0;
    }
    return ans;
}

做一个桶sum[]表示一个数有多少个。

部分分代码(60分)

#include
#define ll long long
using namespace std;
int n,k;
int a[1000001];
int dp[1001][1001];
int sum[10001];

inline ll Read() {
    ll ans = 0;
    char Ch = getchar() , Las = ' ';
    while(!isdigit(Ch)) {Las = Ch;Ch = getchar();}
    while(isdigit(Ch)) {ans = (ans << 3) + (ans << 1) + Ch - '0';Ch = getchar();}
    if(Las == '-') ans = -ans;
    return ans;
}

inline void Write(ll x) {
    if(x < 0) {x = -x;putchar('-');}
    if(x >= 10) Write(x / 10);
    putchar(x % 10 + '0');
}
int dui(int x) {
    x * (x - 1) / 2;
}
int value(int x,int y) {
    int ans = 0;
    memset(sum,0,sizeof(sum));
    int mn = 2e9,mx = 0;
    for(int i = x;i <= y; i++) {
	    sum[a[i]]++;
	    mx = max(mx,a[i]);
	    mn = min(mn,a[i]);
    }
    for(int i = mn;i <= mx; i++) {
	    ans += (sum[a[i]] * (sum[a[i]] - 1) ) / 2;
	    sum[a[i]] = 0;
    }
    return ans;
}

int dfs_dp(int x,int left) {
    if(left == 0) return value(x,n);
    if(left > n - x + 1) return 1e9;
    if(dp[x][left]) return dp[x][left];
    int ans = 2e9;
    for(int i = x;i <= n; i++) {
	    ans = min(ans,value(x,i) + dfs_dp(i + 1,left - 1));
    }
    return dp[x][left] = ans;
}
int flag = 1;
int main() {
    n = Read();
    k = Read();
    for(int i = 1;i <= n; i++) {
	    a[i] = Read();
	    if(a[i]!=a[1]) flag=0;
    }
    if(flag) {
	    ll ans=0;
	    int h = n / k;
	    ans=dui(h + 1) * (n % k) + dui(h) * (k - n % k);
	    Write(ans);
    } else Write(dfs_dp(1,k - 1));
    return 0;
}

全部分

上一个代码超时了。
通过部分分,我们还是可以沿用部分分的思想,进行区间dp,状态转移方程如下:

dp[i][j]=min(dp[i][j],dp[k][j-1]+calc(k+1,i)//calc表示价值

那么对于每一个决策点pl[i],我们不难发现:pl[i - 1] <= pl[i] <= pl[i + 1]
因此我们可以使用决策单调性优化,利用分治思想,暴力求解mid的决策点,从而缩小接下来枚举的范围

void solve(int x,int y,int px,int py) {  //x -> y dp  决策点在px -> py 
    if(x > y) return;
    int mid = x + y >> 1,pl;
    dp[mid][now] = 2e16;
    for(int i = px; i <= min(py,mid - 1); i++) {
	    if(dp[mid][now] > dp[i][now - 1] + calc(i + 1,mid)) {
		    pl = i;  //mid 的决策点(dp转移的位置) 
		    dp[mid][now] = dp[i][now - 1] + calc(i + 1,mid);
	    }
    }
    solve(x,mid - 1,px,pl);
    solve(mid + 1,y,pl,py);
}

求价值也是可以优化的,之前的方法有点慢,我们采用莫队的思想(复杂度很玄,反正就是快了)对其进行优化,其实主要思想还是和桶一样的

ll add(int x) {
    return buc[a[x]]++;
}
ll del(int x) {
    return --buc[a[x]];
}
ll calc(int nl,int nr) {  //i + 1 -> mid 的价值 
    while(r < nr) sum += add(++r);
    while(r > nr) sum -= del(r--);
    while(l > nl) sum += add(--l);
    while(l < nl) sum -= del(l++);
    return sum;
}

AC代码

#include
using namespace std;
#define ll long long
const int N = 1e5 + 100;
int m,n;
int a[N],buc[N];
int l = 1,r = 0;
ll sum = 0;  
ll add(int x) {
    return buc[a[x]]++;
}
ll del(int x) {
    return --buc[a[x]];
}
ll calc(int nl,int nr) {  //i + 1 -> mid 的价值 
    while(r < nr) sum += add(++r);
    while(r > nr) sum -= del(r--);
    while(l > nl) sum += add(--l);
    while(l < nl) sum -= del(l++);
    return sum;
}
ll dp[N][30];
int now;
//pl[i - 1] <= pl[i] <= pl[i + 1] 
void solve(int x,int y,int px,int py) {  //x -> y dp  决策点在px -> py 
    if(x > y) return;
    int mid = x + y >> 1,pl;
    dp[mid][now] = 2e16;
    for(int i = px; i <= min(py,mid - 1); i++) {
	    if(dp[mid][now] > dp[i][now - 1] + calc(i + 1,mid)) {
		    pl = i;  //mid 的决策点(dp转移的位置) 
		    dp[mid][now] = dp[i][now - 1] + calc(i + 1,mid);
	    }
    }
    solve(x,mid - 1,px,pl);
    solve(mid + 1,y,pl,py);
}
int main() {
    scanf("%d%d",&n,&m);
    for(int i = 1; i <= n; i++) {
	    scanf("%d",&a[i]);
    }
    for(int i = 1; i <= n; i++) {
	    dp[i][1] = calc(1,i);
    }
    for(int k = 2; k <= m; k++) {
	    now = k;
	    solve(1,n,0,n);
    }
    printf("%lld\n",dp[n][m]);
    return 0;
}