标签: 动态规划

  • CF1150D Three Religions

    完了我已经掉成Pupil了..

    考虑对于每次询问做一个DP处理。

    设$f(i,j,k)$表示使得串1的前i位,串2的前j位,串2的前k位能在主串中同时不相交出现的最短的前缀长度。

    设$g(n,ch)$表示第n个字符之后,ch第一次出现的位置,如果不存在则设为$\infty$。

    转移时考虑$f(i,j,k)=min(g([i\geq 1]f(i-1,j,k),S_1(i)),g([j\geq 1]f(i,j-1,k),S_2(j)),g([k\geq 1]f(i,j,k-1),S_3(k)))$。

    边界条件$f(0,0,0)=0$。

    可是这样单组询问复杂度是$O(250^3)$的,尽管这也是个常数。

    但是注意到每组询问要么是追加一个字符,要么是删除末尾的字符。

    假设在第i个串后追加一个字符,那么只需要把第i个串的DP数组推进一维即可。

    删除第i个串末尾的字符,只需要把DP数组其他两维置INF。

    #include <iostream>
    #include <algorithm>
    #include <cstdio>
    #include <vector>
    #include <inttypes.h>
    #include <cstring>
    #include <assert.h>
    #define debug(x) std::cout << #x << " = " << x << std::endl;
    
    typedef long long int int_t;
    
    using std::cin;
    using std::endl;
    using std::cout;
    const int_t INF = 0x7fffffff;
    const int_t LARGE = 250;
    //µÚn¸ö×Ö·ûºó£¬×Ö·ûcµÚÒ»´Î³öÏÖµÄλÖÃ
    int first[100001][26];
    int dp[251][251][251];
    int pos[4];
    int strs[4][252];
    char str[100010];
    int n,m;
    int at(int pos,char chr) {
    #ifdef DEBUG
    	cout<<"at "<<pos<<" "<<chr<<" = ";
    #endif
    	if(pos>n) {
    #ifdef DEBUG
    		cout<<0x3f3f3f3f<<endl;
    #endif
    		return 0x3f3f3f3f;
    	}
    #ifdef DEBUG
    	cout<<first[pos][chr-'a']<<endl;
    #endif
    	return first[pos][chr-'a'];
    }
    int main() {
    //    cin >> n >> m;
    	scanf("%d%d%s",&n,&m,str + 1);
    	memset(first[n],0x3f,sizeof(first[n]));
    //	cout<<first[n][1]<<endl;
    	for(int_t i = n; i >= 1; i --) {
    		memcpy(first[i-1],first[i],sizeof(first[i]));
    		first[i-1][str[i]-'a']=i;
    
    	}
    #ifdef DEBUG
    	for(int i=0; i<n; i++) for(char chr='a'; chr<='d'; chr++) cout<<"first "<<i<<" "<<chr<<" = "<<first[i][chr-'a']<<endl;
    
    #endif
    	memset(dp,0x3f,sizeof(dp));
    	dp[0][0][0]=0;
    	for(int i=1; i<=m; i++) {
    		static char opt[3];
    		int id;
    		scanf("%s%d",opt,&id);
    		if(opt[0]=='+') {
    			char chr,strx[3];
    			scanf("%s",&strx);
    			chr=strx[0];
    			strs[id][++strs[id][0]]=chr;
    			int len=strs[id][0];
    			if(id==1) {
    //				for(int i=0; i<=strs[1][0]; i++)
    				{
    					int i=strs[1][0];
    					for(int j=0; j<=strs[2][0]; j++) {
    						for(int k=0; k<=strs[3][0]; k++) {
    							if(i-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i-1][j][k],strs[1][i]));
    							if(j-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j-1][k],strs[2][j]));
    							if(k-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j][k-1],strs[3][k]));
    #ifdef DEBUG
    							cout<<"dp "<<i<< " "<<j<<" "<<k<<" = "<<dp[i][j][k]<<endl;
    #endif
    						}
    					}
    				}
    			} else if(id==2) {
    				for(int i=0; i<=strs[1][0]; i++) {
    //				for(int j=0; j<=strs[2][0]; j++)
    					int j=strs[2][0];
    					{
    						for(int k=0; k<=strs[3][0]; k++) {
    							if(i-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i-1][j][k],strs[1][i]));
    							if(j-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j-1][k],strs[2][j]));
    							if(k-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j][k-1],strs[3][k]));
    #ifdef DEBUG
    							cout<<"dp "<<i<< " "<<j<<" "<<k<<" = "<<dp[i][j][k]<<endl;
    #endif
    						}
    					}
    				}
    			} else {
    				for(int i=0; i<=strs[1][0]; i++) {
    					for(int j=0; j<=strs[2][0]; j++) {
    						int k=strs[3][0];
    //					for(int k=0; k<=strs[3][0]; k++)
    						{
    							if(i-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i-1][j][k],strs[1][i]));
    							if(j-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j-1][k],strs[2][j]));
    							if(k-1>=0) dp[i][j][k]=std::min(dp[i][j][k], at(dp[i][j][k-1],strs[3][k]));
    #ifdef DEBUG
    							cout<<"dp "<<i<< " "<<j<<" "<<k<<" = "<<dp[i][j][k]<<endl;
    #endif
    						}
    					}
    				}
    			}
    		} else {
    
    			if(id==1) {
    				for(int j=0; j<=strs[2][0]; j++)
    					for(int k=0; k<=strs[3][0]; k++) {
    						dp[strs[id][0]][j][k]=INF;
    					}
    			} else if(id==2) {
    				for(int i=0; i<=strs[1][0]; i++)
    //                for(int j=1;j<=strs[2][0];j++)
    					for(int k=0; k<=strs[3][0]; k++) {
    						dp[i][strs[2][0]][k]=INF;
    					}
    
    			} else {
    				for(int i=0; i<=strs[1][0]; i++)
    					for(int j=0; j<=strs[2][0]; j++) {
    //			        for(int k=1;k<=strs[3][0];k++){
    						dp[i][j][strs[3][0]]=INF;
    					}
    
    			}
    			strs[id][0]--;
    		}
    		if(dp[strs[1][0]][strs[2][0]][strs[3][0]]<=n) {
    			printf("yes\n");
    		} else {
    			printf("no\n");
    		}
    	}
    	return 0;
    }

     

  • noi.ac 152 Christmas

    提高组DP吧。

    设$f(n,l,r)$表示从起点出发走到了第n列,并且在这一列的第l行到第r行可以自由走动的最小代价。

    我们对于每一个l,按照从小到大的顺序枚举r,然后先计算出所有区间[l,r]的答案,然后再把一个区间的答案拿去更新它的子区间。

    对于第n列的区间$[l,r]$,如果任意一个位置能被第n+1列的某个棋子控制,那么这个状态不合法。

    同理如果这个区间不被任意一个第n-1列的棋子控制,那么$f(n,l,r)$的答案可以从$f(n-1,k,k)$,其中$k\in [l,r]$转移而来。

    如果被至少一个棋子控制,那么设x为棋子行编号的最小值,y为棋子行标号的最大值,显然我们要保证第n-1列[x,y]之间的区域能自由走动,故可以从$f(n-1,x,y)$转移而来。

    但是有一个特例,如果第n列区间$[l,r]$被第n-1列l-1行的棋子控制住,那么我们不能从$f(n-1,l-1,l-1)$转移而来(否则跳过了第n-1列第l行这个棋子,直接飞过来的?),应该从$f(n-1,l-1,l)$转移而来。

    计算完第n列所有区间的答案后,再跑一遍朴素区间DP,把大区间的答案传递给他所有的子区间即可。

    边界条件:

    初始时所有DP值为INF,$f(0,n,n)=0$。

    #include <algorithm>
    #include <cstring>
    #include <iostream>
    using int_t = long long int;
    using std::cin;
    using std::cout;
    using std::endl;
    // f(n,l,r) 前n列,让纵坐标区间[l,r]可自由走动的最小代价
    int_t dp[1001][101][101];
    int_t mat[101][1001];
    int_t n, m;
    const int_t INF = 0x7fffffff;
    int main() {
        scanf("%lld%lld", &n, &m);
        for (int_t i = 1; i <= n; i++) {
            static char buf[10001];
            scanf("%s", buf + 1);
            for (int_t j = 1; j <= m; j++) {
                mat[i][j] = buf[j] - '0';
            }
        }
        memset(dp, 0x3f, sizeof(dp));
        //当前位置左上是否有东西
        const auto blockByLeft = [&](int_t r, int_t c) {
            r -= 1, c -= 1;
            if (r >= 1 && r <= n && c >= 1 && c <= m) return (bool)mat[r][c];
            return false;
        };
        const auto blockByRight = [&](int_t r, int_t c) {
            r -= 1, c += 1;
            if (r >= 1 && r <= n && c >= 1 && c <= m) return (bool)mat[r][c];
            return false;
        };
        if (blockByRight(n, 1)) {
            cout << -1 << endl;
            return 0;
        }
        dp[0][n][n] = 0;
        for (int_t i = 1; i <= m; i++) {
            //枚举上端点
            for (int_t j = 1; j <= n; j++) {
                int_t sum = 0;
                //左侧列屏蔽当前点的最大最小行编号
                int_t minblock = INF, maxblock = 0;
                int_t minval = INF;
                bool hasblock = false;
                for (int_t k = j; k <= n; k++) {
                    minval = std::min(minval, dp[i - 1][k][k]);
                    if (blockByRight(k, i)) break;
                    sum += mat[k][i];
                    if (blockByLeft(k, i)) {
                        minblock = std::min(minblock, k - 1);
                        maxblock = std::max(maxblock, k - 1);
                        hasblock = true;
                    }
                    if (!hasblock) {
                        dp[i][j][k] = std::min(dp[i][j][k], minval + sum);
                    } else {
                        dp[i][j][k] = std::min(
                            dp[i][j][k],
                            sum + dp[i - 1][minblock][std::max(j, maxblock)]);
                    }
                }
            }
            //长度
            for (int_t j = n; j >= 1; j--) {
                //左端点
                for (int_t k = 1; j + k - 1 <= n; k++) {
                    dp[i][k + 1][k + j - 1] =
                        std::min(dp[i][k + 1][k + j - 1], dp[i][k][k + j - 1]);
                    dp[i][k][k + j - 2] =
                        std::min(dp[i][k][k + j - 2], dp[i][k][k + j - 1]);
                }
            }
        }
        int_t result = INF;
        for (int_t i = 1; i <= n; i++) {
            for (int_t j = i; j <= n; j++) {
                result = std::min(result, dp[m][i][j]);
            }
        }
        if (result > n * m) {
            result = -1;
        }
        cout << result << endl;
    
        return 0;
    }

     

  • 九省联考2018 秘密袭击

    $$\text{枚举权值}i\text{,然后计算第}k\text{大值为}i\text{的联通块个数} \\ \text{这个等价于第}k\text{大值大于等于}i\text{的联通块个数} \\ \text{如果一个联通块第}k\text{大的值大于等于}i\text{,那么这个联通块中大于等于}i\text{的值一定出现了至少}k\text{次} \\ \text{所以我们可以枚举}i\text{,然后对于每一个}i\text{,将所有权值小于}i\text{的点设置为0,大于等于}i\text{的点设置为}1 \\ \text{然后统计权值和大于等于}k\text{的联通块个数。} \\ \text{对于每一个}i\text{,我们可以通过树形}DP\text{去统计} \\ \\ \text{设}f\left( vtx,k \right) \text{表示以}vtx\text{为根的子树中,经过}vtx\text{,权值和为}k\text{的联通块个数} \\ f\left( vtx,k \right) =\prod_{k_1+k_2+k_3+….+k_n=k}{f\left( v_i,k_i \right)} \\ \text{边界条件:}vtx\text{的值为0时,}f\left( vtx,0 \right) =\text{1,}vtx\text{的值为1时,}f\left( vtx,1 \right) =1 \\ \text{然后转移的时候通过一个类似于背包的东西就行转移即可。} \\ \text{注意状态的枚举顺序和枚举上界,见代码}. \\ \text{其中}n\text{为}vtx\text{的子节点数,}v_i\text{取遍}vtx\text{的子节点}$$

    #pragma GCC optimize("O3")
    #include <iostream>
    #include <algorithm>
    #include <vector>
    #include <cstring>
    
    using int_t = int;
    using std::cin;
    using std::cout;
    using std::endl;
    
    const int_t mod = 64123;
    const int_t LARGE = 1670;
    int_t n, k, w;
    
    int_t val[LARGE + 1];
    std::vector<int_t> graph[LARGE + 1];
    int_t size[LARGE + 1];
    int_t suffix[LARGE + 1];
    int_t dp[LARGE + 1][LARGE + 1];
    //计算以vtx为根的子树中,出现大于等于limit的权值次数大于等于k的方案数
    int_t DFS(int_t vtx, int_t limit, int_t from = -1)
    {
        int_t result = 0;
        size[vtx] = (val[vtx] >= limit);
        dp[vtx][size[vtx]] = 1;
        for (int_t to : graph[vtx])
        {
            if (to == from)
                continue;
            result = (result + DFS(to, limit, vtx)) % mod;
            //注意要从大到小枚举,防止算重
            for (int_t i = size[vtx]; i >= 0; i--)
            {
                if (dp[vtx][i] != 0)
                {
                    //j=0的时候可能会导致算重,所以仍然要倒着枚举
                    for (int_t j = size[to]; j >= 0; j--)
                    {
                        dp[vtx][i + j] = (dp[vtx][i + j] + 1u * dp[vtx][i] * dp[to][j] % mod) % mod;
                    }
                }
            }
            size[vtx] += size[to];
        }
        for (int_t i = k; i <= size[vtx]; i++)
            result = (result + dp[vtx][i]) % mod;
        return result;
    }
    
    int main()
    {
        cin >> n >> k >> w;
        for (int_t i = 1; i <= n; i++)
        {
            cin >> val[i];
            suffix[val[i]]++;
        }
        for (int_t i = 1; i <= n - 1; i++)
        {
            int_t from, to;
            cin >> from >> to;
            graph[from].push_back(to);
            graph[to].push_back(from);
        }
        for (int_t i = w - 1; i >= 1; i--)
        {
            suffix[i] += suffix[i + 1];
        }
        int_t result = 0;
        for (int_t i = 1; i <= w; i++)
        {
            //一个小剪枝
            if (suffix[i] < k)
                continue;
            memset(dp, 0, sizeof(dp));
            int_t curr = DFS(1, i);
            result = (result + curr) % mod;
        }
        cout << result << endl;
        return 0;
    }

     

  • NOIP2017 宝藏

    $$ f\left( vtx,len,set \right) \text{表示当前在点}vtx\text{,已经挖了长度为}len\text{的道路,要去挖通}set\text{这些点的最小代价} \\ \text{转移:} \\ \text{对于每一个}vtx\text{,枚举出边,对于每一个出边}\left( vtx,to \right) \text{,枚举要从这个点去挖的子集}\left( \text{这个子集必须要包含}to \right) \text{,然后统计上挖这条边的代价。} \\ \\ f\left( vtx,len,set \right) =MIN_{S\subset \ set\ and\ vtx\ \notin \ S}MIN_{edge\ \left( vtx,to \right) \ and\ to\in \ S}f\left( to,len+\text{1,}S \right) \ +\ f\left( vtx,len,S^{set} \right) \ +\left( len+1 \right) \times weight\left( vtx,to \right) \ \\ \text{边界条件:} \\ f\left( vtx,len,0 \right) =0 \\ \text{挖空集的代价为}0 \\ $$

     

    #include <iostream>
    #include <algorithm>
    #include <cstdio>
    #include <cstdlib>
    #include <vector>
    #include <cstring>
    #include <string>
    typedef  int int_t;
    
    using std::cin;
    using std::endl;
    using std::cout;
    
    const int_t LARGE = 20;
    const int_t INF = 0x3f3f3f3f;
    
    
    int_t n,m;
    
    int_t graph[LARGE + 1][LARGE + 1];
    int_t memory[LARGE + 1][LARGE + 1][1 << 13];
    bool visited[LARGE + 1][LARGE + 1][1 << 13];
    int_t* subsets[1 << 13];
    struct Edge{
        int_t to;
        int_t weight;
        int_t next;
        Edge(int_t to = 0,int_t weight = 0,int_t next = 0){
            this -> to = to;
            this -> weight = weight;
            this -> next = next;
        }
    } edges[LARGE * LARGE + 1];
    int_t next = 1;
    int_t head[LARGE + 1];
    void pushEdge(int_t from,int_t to,int_t weight){
        int_t id = next++;
        edges[id] = Edge(to,weight,head[from]);
        head[from] = id;
    }
    
    int_t search(int_t vtx,int_t len,int_t set) {
    	//要去挖空集
    	if(set == 0) return memory[vtx][len][set] = 0;
    	//已经挖了长度为n - 1的链
    	if(len == n - 1) {
    		if(set != 0) return memory[vtx][len][set] = INF;
    	}
    	int_t& result = memory[vtx][len][set];
    	if(visited[vtx][len][set]) return result;
    	visited[vtx][len][set] = true;
    	//不要把结果在记忆化返回前赋值 
    	result = INF;
    	for(int_t ptr = head[vtx];ptr != 0; ptr = edges[ptr].next) {
    	    int_t to = edges[ptr].to;
    	    int_t weight = edges[ptr].weight;
    		//枚举出边
    		if(to == vtx  || (set & (1 << to)) == 0) continue;
    		//枚举要挖的子集
    		//不能去挖空集
    		for(int_t k = 1; k <= subsets[set][0]; k ++) {
    			int_t x = subsets[set][k];
    			if(x & (1 << to)) {
    				result = std::min(result,search(to , len + 1, x & (~(1 << to))) + weight * (len + 1) + search(vtx,len,set & (~(x))));
    			}
    		}
    	}
    	return result;
    }
    
    int main() {
    	cin >> n >> m;
    	for(int_t i = 0; i < n; i++) {
    		for(int_t j = 0; j < n; j++) {
    			graph[i][j] = INF;
    		}
    	}
    	for(int_t i = 1; i <= m; i ++) {
    		int_t from,to,weight;
    		cin >> from >> to >> weight;
    		from -= 1;
    		to -= 1;
    		graph[from][to] = std::min(graph[from][to],weight);
    		graph[to][from] = graph[from][to];
    	}
    	for(int_t i = 0;i < n;i++){
    	    for(int_t j = 0;j < n;j++){
    	        if(graph[i][j] < INF && i != j){
    	            pushEdge(i,j,graph[i][j]);
                }
            }
        }
        static int_t subset[1 << 13];
        for(int_t i = 1 ;i < (1 << n);i ++){
            
            for(int_t j = 1;j <= i;j++){
                if((j & i) == j) subset[++subset[0]] = j;
            }
            std::sort(subset + 1,subset + 1 + subset[0]);
            int_t* tail = std::unique(subset + 1,subset + 1 + subset[0]);
            subsets[i] = new int_t[tail - subset];
            memcpy(subsets[i],subset,sizeof(int_t) * (tail - subset));
            subset[0] = 0;
        }
    	int_t result = INF;
    	for(int_t i = 0; i < n; i++) {
    	    int_t temp = search(i,0,((1 << n) - 1) ^ (1 << i));
    		result = std::min(result,temp);
    	}
    	cout << result << endl;
    	return 0;
    }

     

  • TopCoder 12329 SpellCards

    题目可以转化为01背包。

    将level视为体积,damage视为价值,进行容量为n的01背包。

    关于证明,任何一个我们选择要使用的卡片序列,将其倒序使用,如果这些卡片的level之和小于等于n,那么每一张卡片的使用一定不会影响到其他卡片的使用。

    #include <vector>
    #include <algorithm>
    #include <iostream>
    #include <cstring>
    using std::cin;
    using std::cout;
    using std::endl;
    int f[100];
    
    class SpellCards
    {
      public:
        int maxDamage(std::vector<int> level, std::vector<int> damage)
        {
            memset(f, 0, sizeof(f));
            int n = level.size();
            for (int i = 0; i < n; i++)
            {
                for (int j = n; j >= level[i]; j--)
                {
                    f[j] = std::max(f[j], f[j - level[i]] + damage[i]);
                }
            }
            return *std::max_element(f, f + 1 + n);
        }
    };
    #ifdef DEBUG
    int main()
    {
        int n;
        cin >> n;
        std::vector<int> level, damage;
        for (int i = 1; i <= n; i++)
        {
            int x;
            cin >> x;
            level.push_back(x);
        }
        for (int i = 1; i <= n; i++)
        {
            int x;
            cin >> x;
            damage.push_back(x);
        }
        cout << SpellCards().maxDamage(level, damage);
        return 0;
    }
    #endif

     

  • APIO2014 序列分割

    很简单的斜率优化题(捂脸)

    $$ \text{状态转移方程} \\ f\left( k,n \right) \text{表示在前}n\text{个数中切了}k\text{刀的最优解} \\ \left( \text{对于}n\le k\text{或者}k=\text{0,结果为}0 \right) \\ \text{设}S_n=\sum_{1\le i\le n}{a_i} \\ f\left( k,n \right) =MAX_{1\le i\le n-1}f\left( k-\text{1,}i \right) +\left( S_n-S_i \right) S_i \\ \text{以}k\text{为阶段进行转移} \\ \text{在从}k-\text{1转移到}k\text{时,维护一个单调队列,从队尾到队首的解越来越差} \\ \text{如何构造这个单调队列?} \\ \text{假设单调队列中的所有的元素已经有序,设其中最后的两个元素的}n\text{的取值为}i,j\ \text{设我们要加入到队列的元素的}n\text{的取值为}k \\ \text{设}g\left( x \right) =f\left( k-\text{1,}x \right) -S_{x}^{2} \\ \text{以为单调队列前面的元素比后面的优,所以}i\text{是比}j\text{优的} \\ \text{假如现在}k\text{还比}j\text{优,那么我们显然可以直接把}j\text{扔了,因为它没用} \\ \text{考虑什么时候}k\text{比}j\text{优} \\ \text{如果}i\text{比}j\left( i<j \right) \text{优,那么一定有} \\ f\left( k-\text{1,}i \right) +S_nS_i-S_{i}^{2}>f\left( k-\text{1,}j \right) +S_nS_j-S_{j}^{2} \\ \text{整理一下} \\ f\left( k-\text{1,}i \right) -S_{i}^{2}-\left( f\left( k-\text{1,}j \right) -S_{j}^{2} \right) >S_n\left( S_j-S_i \right) \\ g\left( i \right) -g\left( j \right) >S_n\left( S_j-S_i \right) \\ i<j,\text{所以}S_i<S_j\ \left( S_n\text{单调不降} \right) \\ \text{所以不等式不要变号} \\ \frac{g\left( i \right) -g\left( j \right)}{S_j-S_i}>S_n \\ \text{两边取负} \\ \frac{g\left( i \right) -g\left( j \right)}{S_i-S_j}<-S_n \\ \text{同理,如果}k\text{优于}j\text{,那么我们可以得到} \\ \frac{g\left( k \right) -g\left( j \right)}{S_k-S_j}>-S_n \\ \text{所以我们可以得到,如果}i\text{优于}j\text{而且}k\text{优于}j\left( \text{这个情况应该扔掉}j \right) \ i,j,k\text{应该满足的条件是} \\ \frac{g\left( k \right) -g\left( j \right)}{S_k-S_j}>\frac{g\left( i \right) -g\left( j \right)}{S_i-S_j} \\ \text{即}k,j\text{所构成的直线的斜率大于}i,j\text{所构成的直线的斜率} \\ \text{所以在压入元素}k\text{时,我们需要一直判断直线}ij\text{和直线}jk\text{的斜率关系,如果满足直线}jk\text{的斜率大于直线}ij\text{的斜率,那么我们就需要扔掉点}j \\ \text{一直重复,直到队列中只剩下一个点或者}jk\text{的斜率小于等于}ij\text{的斜率} \\ \text{同时,每个}n\text{的取值在计算时,我们要做的是从队列头取元素} \\ \text{然而}i<j\text{且}i\text{优于}j\text{的条件是} \\ \frac{g\left( i \right) -g\left( j \right)}{S_i-S_j}<-S_n \\ \text{所以我们需要保证队头的元素满足这个关系} \\ \text{所以我们每次考虑队头的两个元素}i,j\text{,如果不满足这个关系,那么就扔掉}i\text{,因为这个时候}i\text{不可能比}j\text{优} \\ \text{然后转移即可} \\ \text{单调队列中的点构成了一个上凸壳,相邻两点所连成的直线,从左到右满足斜率单调不升} \\ \\ $$

    #include <iostream>
    #include <algorithm>
    #include <deque>
    #include <cstring>
    using int_t = long long int;
    using real_t = long double;
    
    using std::cin;
    using std::cout;
    using std::endl;
    
    const int_t LARGE = 100000;
    int_t sum[LARGE + 1];
    int n, k;
    const real_t EPS = 1e-12;
    struct Point
    {
        //g(pos)
        real_t value;
        //n的取值
        int_t pos;
        Point(real_t val = 0, int_t pos = 0)
        {
            this->value = val;
            this->pos = pos;
        }
    };
    
    real_t scope(const Point &a, const Point &b)
    {
        if (sum[a.pos] == sum[b.pos])
            return (real_t)(a.value - b.value) / EPS;
        return (real_t)(a.value - b.value) / (real_t)(sum[a.pos] - sum[b.pos]);
    }
    int_t dp[201][LARGE + 1];
    int_t path[201][LARGE + 1];
    Point queue[LARGE * 2 + 1];
    int main()
    {
        scanf("%d%d", &n, &k);
        for (int_t i = 1; i <= n; i++)
        {
            scanf("%lld", &sum[i]);
            sum[i] += sum[i - 1];
        }
    
        //左闭右开区间
        int_t head = 0;
        int_t tail = 0;
        for (int_t k = 1; k <= ::k; k++)
        {
            //每次开始时清空队列
            head = tail = 0;
            //把唯一一个合法的点扔进去
            queue[tail] = Point(dp[k - 1][k] - sum[k] * sum[k], k);
            tail++;
            for (int_t n = k + 1; n <= ::n; n++)
            {
                /*
                g(n)=f(k-1,n)-S(n)^2
                */
                //弹出不合法的元素
                while (tail - head >= 2 && scope(queue[head], queue[head + 1]) >= -sum[n])
                    head++;
                //写结果
                int_t prev = queue[head].pos;
                dp[k][n] = dp[k - 1][prev] + (sum[n] - sum[prev]) * sum[prev];
    #ifdef DEBUG
                cout << "(k=" << k << " n= " << n << ")=" << dp[k][n] << " from (k=" << k - 1 << " n= " << prev << ") = " << dp[k - 1][prev] << endl;
    #endif
                path[k][n] = prev;
                //加入新的元素
                Point curr(dp[k - 1][n] - sum[n] * sum[n], n);
                while (tail - head >= 2 && (sum[curr.pos] == sum[queue[tail - 1].pos] || scope(curr, queue[tail - 1]) >= scope(queue[tail - 1], queue[tail - 2])))
                {
                    tail--;
                }
                queue[tail] = curr;
                tail++;
            }
    #ifdef DEBUG
            cout << endl;
    #endif
        }
    
        printf("%lld\n", dp[k][n]);
        int_t id = path[k][n];
        for (int_t k = ::k; k >= 1; k--)
        {
            printf("%lld ", id);
            id = path[k - 1][id];
        }
        return 0;
    }