• 「NOI Online 提高组 」T3 最小环 70分题解


    题目链接

    c67625086b63f624199efc639044ebf81b4ca37e.jpg

    这篇文章中, “基本思路”部分会详细介绍我最真实的推理过程,可能会有些啰嗦,还会包括一些显而易见随手推一推就能推出来的结论的证明,这些结论我已经用粗体字标注出来了。

    基本思路

    观察样例,显然

    当n可以被k整除时,整个环可以拆分成k个互不相关的环。

    样例中,k=2时,可以将最优方案看成({3,1,2})({6,4,5})两个集合中任意两个距离为1的数的乘积的和。

    当一个环中只有三个数,并且k=1(也就是要求相邻两个数乘积的和)时,三个数的位置并不会影响最终结果,因为每个数都会和另外两个数分别乘一次。所以把三个最小的数 和三个最大的数 组合在一起,就是最优解。

    这个结论比较容易想到,以下是证明过程:

    n=6,k=3 时的证明

    尝试把两个集合中的任意一个数互换位置。比如互换1和6,那么两个集合就会变成({3, 6, 2})({1, 4, 5}),比较第一个集合算出的结果在这次交换后上升的值 和 第二个集合(算出的结果)在交换后下降的值,它们分别为((6-1)*(3+2))((6-1)*(4+5)),不论交换哪两个数,两个式子第一个乘数都相等。而第二个乘数中,第二个集合总比第一个集合大。综上所述,第一个集合上升的值比第二个集合下降的值要小,它们的和会下降。

    而我们很容易把这个证明推广到正整数范围。

    那么当n不能被k整除时呢?

    先说结论:当k不能整除n时,只要n不变,最大值就确定

    理论

    还是举个例子:

    比如n=7, k=3时,这个环为({1, 2, 3, 4, 5, 6, 7})时,算出的和为:

    $14+47+73+36+62+25+5*1$

    显然(其实我也不知道是为什么),没有重复和遗漏,并且按照这个顺序,我们也可以把它看成7个数的环中,相邻的两个数乘积的和,只不过是这个“相邻”指的是距离为k的两个数。

    和传统意义上的“相邻”(即k=1)一样,这个环上的每个数有且仅有两个数和它相乘。对于这道题,唯一的不同点在于,对于同样的一个环,k会影响其最后算出的和。但是对于本题中求的最大值,我们可以在算出k=1(因为这比较方便计算)后再去调整“相邻”

    例子:如何变换

    比如n=7,k=1时,这个环是这样的:

    ({1, 2, 3, 4, 5, 6, 7})

    算出的和为:

    (1*2+2*3+3*4+4*5+5*6+6*7+7*1)

    当k=3时,如何变换这个环,使算出来的值等于k=1是的值呢?(以下步骤使用x表示未知数)

    // 首先,将1(第1个数,后同理)填入任何一个位置(因为是一个环,首尾相接,绝对未知并不会影响最终结果。为方便演示,填到[0]的位置)
    
    {1, x, x, x, x, x, x}
    
    // 找到与1相邻的位置,填2
    
    {1, x, x, 2, x, x, x} 或 {1, x, x, x, 2, x, x}  //两种分别对应往前数和往后数,后省略,以第一种为例
    
    // 找到另一个与2相邻的位置(即两个位置中未知的那个),填3
    
    {1, x, x, 2, x, x, 3}
    
    // 以此类推,找到另一个与n相邻的未知,填(n+1)
    
    {1, x, 4, 2, x, x, 3}
    {1, x, 4, 2, x, 5, 3}
    {1, 6, 4, 2, x, 5, 3}
    {1, 6, 4, 2, 7, 5, 3}
    

    相信在这个过程中,可以生动体会到上面的理论

    于是,将这个环代入计算,过程和结果与上面的完全一致。

    找出最大值

    上面说了那么多,总结起来一句话:当n与环a一定,且k不整除n时,最大值不随k变化而变化(k这时可以当作1)

    而当k整除n时,又可以拆成k个小环,而每个子环都可以看成是(n=(n/k)),$ k=1$的环。

    这样,我们就把两种情况统一成一种了。

    唯一例外的是,当k为一个完全平方数,且sqrt(k)整除n时,可以看作是k=sqrt(k)

    现在要实现的就是,当k=1时,怎样排列可以使这个值最大。

    实例

    可以参考样例中k=1, n=6时的排列:

    可以发现和6相邻的为4和5,和5相邻的另一个数是3,和4相邻的另一个数是2。

    这样排列把最大的数放在了一起,然后从大到小排列,尽可能把大的数放在一起。我会用n=7, k=1来演示这个过程

    {x, x, x, x, x, x, x} //初始状态
    
    // 在任意位置放7,并且时6,5与7相邻
    
    {x, x, 5, 7, 6, x, x}
    
    // 在6旁边放剩余的最大的数
    
    {x, x, 5, 7, 6, 4, x}
    
    // 在5旁边放剩余最大的数
    
    {x, 3, 5, 7, 6, 4, x}
    
    // 重复这一过程,每次将 相邻位置有空位的最大数 的 另一个位置填上剩余最大数
    
    {x, 3, 5, 7, 6, 4, 2}
    {1, 3, 5, 7, 6, 4, 2}
    

    这种方法貌似时有一种贪心思想在内,要想证明这种方法有效, 可以用类似上面的证明方法,这里不再赘述。

    实际应用与代码

    在多举几个例子后,会发现,在代码中,并不需要使用一个数组来真正存储它,只需要实现这样一个函数:

    unsigned long long ch(unsigned long long l){
    	unsigned long long aa=0;
    	aa+=a[z]*a[z+1];
    	for(int i=0; i<l-2; i++){
    		aa+=a[z]*a[z+2];
    		z++;
    	}
    	aa+=a[z]*a[z+1];
    	z+=2;
    	return aa;
    }
    

    基本原理就是上面的一大堆,自己稍微推一下就会得出这个算法。

    代码

    #include <iostream>
    #include <cstdio>
    #include <cmath>
    #include <algorithm>
    using namespace std;
    
    const int len=2*1e5+5;
    unsigned long long n, m, k, z, re;
    unsigned long long ans, a[len];
    bool cmp(int a, int b) { return a>b; }
    unsigned long long ch(unsigned long long l){
    	unsigned long long aa=0;
    	aa+=a[z]*a[z+1];
    	for(int i=0; i<l-2; i++){
    		aa+=a[z]*a[z+2];
    		z++;
    	}
    	aa+=a[z]*a[z+1];
    	z+=2;
    	return aa;
    }
    
    int main(){
    	freopen("ring.in", "r", stdin);
    	freopen("ring.out", "w", stdout);
    	scanf("%lld %lld", &n, &m);
    	for(int i=0; i<n; i++){
    		scanf("%lld", &a[i]);
    	}
    	sort(a, a+n, cmp);
    	for(int i=0; i<m; i++){
    		scanf("%lld", &k);
    		ans=0;
    		if((k!=0) && ((n/k)*k!=n) && (int(sqrt(k))*int(sqrt(k))==k)) k=int(sqrt(k));
    		if(k==0){
    			for(int j=0; j<n; j++) ans+=a[j]*a[j];
    		}
    		else if((n/k)*k==n){
    			z=0;
    			for(int j=0; j<k; j++){
    				ans+=ch(n/k);
    			}
    		}
    //		else if((n/sqrt(k))*sqrt(k)==n){
    //			k=sqrt(k);
    //			z=0;
    //			for(int j=0; j<k; j++){
    //				ans+=ch(n/k);
    //			}
    //		}
    		else{
    			if(re>0) ans=re;
    			else{
    				z=0;
    				ans=ch(n);
    				re=ans;
    			}
    		}
    		printf("%lld
    ", ans);
    	}
    	
    	return 0;
    }
    
    
  • 相关阅读:
    KETTLE封装
    基于MODBUS-RTU协议的串口编程
    阿里DRUID 配置说明及分析
    CopyOnWriteArrayList集合排序异常问题
    CopyOnWriteArrayList集合线程安全解释
    JAR包数字签名与验证
    MySQL中select * for update锁表的范围
    Kettle文本文件输出和输入控件使用中,换行符导致的问题处理
    UAP如何根据DeviceFamily显示不同的页面
    Windows 10 响应式设计和设备友好的开发
  • 原文地址:https://www.cnblogs.com/dong628/p/12488911.html
Copyright © 2020-2023  润新知