【NOIP模拟】20140817 题解 & 总结

  今天来总结一套接近一个月之前(817)做的题- -,因为各种各样的理由忘了总结,现在补上……不过做题时的感觉都忘了,只能好好回顾回顾了。

 

  T1Magical GCD

  题目大意:对于一个由正整数组成的序列,Magical GCD是指一个区间的长度乘以该区间内所有数字的最大公约数。给出T个含有N个数的序列(N<=100000T<=20,每个数<=10^12),求出这个序列最大的Magical GCD

  做这道题的时候好像是一点想法都没有……果断暴力了……

  如果我们考虑一个区间 [l,r] 内所有数的最大公约数,如果这个区间越来越大,那么最大公约数只能是越来越小。因此,我们可以考虑将这N个数划分成若干个区域,此时每个区域内的数都是相同的。然后我们合并这些区域,可以发现新区域的最大公约数可以秒解,区间长度也可以通过合并得到。在不断的合并中我们就可以遍历完所有可能的答案了,找答案的最大值就可以了。

  因为合并过程中最多会有log级别的区域,因此时间复杂度为O(T*NlogK),其中KN个数中最大的数。

var
    ans:int64;
    t,n,i,j,c:longint;
    a:array[1..100000] of int64;
    f:array[1..100000,1..2] of longint;
function max(x,y:int64):int64; begin if x>y then exit(x) else exit(y); end;
function gcd(a,b:int64):int64; begin if b=0 then exit(a) else exit(gcd(b,a mod b)); end;
procedure add(x,y:longint);
    begin
        inc(c);
        a[c]:=gcd(a[x],a[y]);
        f[c,1]:=f[x,1];
        f[c,2]:=f[y,2];
        ans:=max(ans,a[c]*(f[c,2]-f[c,1]+1));
    end;
begin
    assign(input,'magicgcd.in');reset(input);
    assign(output,'magicgcd.out');rewrite(output);
    readln(t);
    for t:=1 to t do begin
        readln(n);
        ans:=1;
        for i:=1 to n do begin
            read(a[i]);
            f[i,1]:=i;
            f[i,2]:=i;
            ans:=max(ans,a[i]);
        end;
        while n>1 do begin
            j:=1;
            c:=0;
            for i:=2 to n do
                if a[j]<>a[i] then begin
                    if i=j+1 then add(j,i)
                    else begin
                        add(j,i-1);
                        add(i-1,i);
                    end;
                    j:=i;
                end;
            if j
   
   

  T2Multiset

  题目大意:有个集合,初始只有一个数0。对于集合中的每个数x有下列3个操作:

  1、x = x + 1

  2、x = y + zx分裂为两个自然数yz);

  3、什么都不做

  对集合中所有的数都选择一个操作执行,称为对集合进行了“一轮”操作。现在给出最终集合中的N个数(N<=10^6x<=10^6),求最少已经进行了几轮操作。

  考试我对这道题很有想法,花了很长的时间做,如愿AC

  因为给出的3种操作都是可逆的,所以我们可以考虑对最终集合“倒着”操作。现在问题当然就是如何选择操作才能使轮数最少。

  如果有任意两个数是0,那么当前这一轮就合并所有的0就行了。关键的问题是:对于一些非0的数,是先和其他的数(包括0)合并,还是减1?仔细想想就可以知道,选择减1较优,因为如果两个非0的数各自减1后合并,2轮就可以完成;如果这两个数先合并再减2,就需要3轮了。因为合并两个数不能让两个数同时执行减1操作。

  因此,对于所有非0的数,我们不断减1,当减到0时就和其他的0合并,如此操作直到集合只剩下一个0为止。此时所花费的轮数就是答案了。

  可以发现这样模拟的最坏时间复杂度为O(Ans*N),其中Ans为答案。看起来会爆,但因为我们最多只能进行Max(x)+log(N)轮操作,而每次操作时真正要修改的数也只是不断减少,所以还是很快的。

  PS:因为模拟本质还是减数,所以可以直接用桶存下所有的数再统计轮数,数学方法秒杀模拟。本人太懒,就把DengWx的程序搬过来吧……

var
    a:array[0..1000010] of longint;
    n,i,j,c,m,t,s,ans:longint;
procedure qsort(x,y:longint);
    var
        i,j,m:longint;
    begin
        if x>=y then exit;
        i:=x;
        j:=y;
        m:=a[random(y-x+1)+x];
        repeat
            while a[i]
    
    
     
     m do dec(j);
            if i<=j then begin
                a[0]:=a[i];
                a[i]:=a[j];
                a[j]:=a[0];
                inc(i);
                dec(j);
            end;
        until i>j;
        qsort(x,j);
        qsort(i,y);
    end;
begin
    randomize;
    assign(input,'multiset.in');reset(input);
    assign(output,'multiset.out');rewrite(output);
    readln(n);
    for i:=1 to n do read(a[i]);
    qsort(1,n);
    m:=1;
    while (m<=n) and (a[m]=0) do inc(m);
    c:=m-1;
    ans:=0;
    s:=0;
    while (c>1) or (m<=n) do begin
        inc(ans);
        c:=c div 2+c mod 2;
        t:=a[m]-s;
        s:=a[m];
        j:=0;
        while (m<=n) and (a[m]-s=0) do begin
            inc(j);
            inc(m);
        end;
        {for i:=i to n do begin
            dec(a[i],t);
            if a[i]=0 then inc(j);
            if (a[i]>0) and (m=0) then m:=i;
        end;
        if m=0 then m:=n+1;}
        for i:=1 to t-1 do c:=c div 2+c mod 2;
        c:=c+j;
        if t>0 then inc(ans,t-1);
    end;
    writeln(ans);
    close(input);close(output);
end.

    
    

  DengWx的源代码:
#include 
    
    
     
     
#include 
     
     
      
      
using namespace std;
const int N = 1000010;

int n, a[N], b[N], maxn;

int main(){
	ios::sync_with_stdio(0);
	freopen("multiset.in","r",stdin);
	freopen("multiset.out","w",stdout);
	scanf("%d", &n);
	for (int i = 1; i <= n; ++ i) scanf("%d", &a[i]), ++ b[a[i]], maxn = maxn < a[i] ? a[i] : maxn;
	int ans(b[0]);
	for (int i = 1; i <= maxn; ++ i){
		ans -= ans / 2; ans += b[i];
	}
	while (ans - 1) ans -= ans / 2, ++ maxn;
	printf("%d", maxn);
	return 0;
}

     
     
    
    

  T3:组队

  题目大意:给出N个人之间的关系(N<=100),只有认识与不认识两种,且为单向认识,无传递性。现在要将这些人分成两组,要求每个组中至少有1个人,每个组中人都互相认识,且两组之间的人数差最小。输出其中一种分法。

  做这道题时也没什么想法,直接暴力搞起……

  其实给出的这张关系图是很明显的二分图,如果不是二分图那就意味着没有任何一种合法的分法,无解。对这张图染色后就可以使用动态规划方程找方案了。

  还是具体点说:如果两个人之间只要有一个人不认识另一个人,那么这两个人就必须分开在两个组里,因此只要不是互相认识的两个人u,v,我们就给这两个人连一条无向边。这样构成的新图中两个相邻的点都必须被染上不同的颜色(一共只有2种颜色)。因此使用深搜尝试给每个点染色,如果发现染色冲突,那么就是无解的情况了。

  这样一来,我们将图中的每个连通块都染上了颜色,每个连通块中的相同颜色的点要被放在同一组中。现在我们考虑——应该怎么分配这些点呢?

  因为每个连通块中不同颜色的点数差是已知的(深搜时可以顺便统计一下),那么我们只要枚举其中一个颜色的点放在哪一组,那么两个组之间的点数差是可以马上知道的。那么此时就要动态规划了:F[i][j]表示将第i个连通块中的点放入两个组后,两组间点数差为j的方案是否存在(注意,j是可以为负数的)。那么转移是显然的:只要f[i-1,j±q[i]]trueq[i]为第i个连通块中不同颜色的点数差)那么f[i,j] = true。此时只要再开一个g[i,j]记录f[i,j]是从哪个状态转移来的,就构成了一条链。此时枚举0<=j<=n,当f[n,±j]true时就代表两组间点数差为j的方案存在,根据g的记录就可以还原所有分配操作,从而得到两组具体的答案了。

  时间复杂度最坏O(N^2)

var
	a:array[1..100,1..100] of boolean;
	pd:array[1..100] of boolean;
	f,l:array[0..100,-200..200] of boolean;
	b,p,q,ans1,ans2:array[0..100] of longint;
	n,i,j,k,c:longint;
procedure dye(v:longint);
	var
		i,nxt:longint;
	begin
		p[v]:=c;
		nxt:=3-b[v];
		if b[v]=1 then inc(q[c]) else dec(q[c]);
		for i:=1 to n do
			if a[v,i] then begin
				if b[v]=b[i] then begin
					writeln(-1);
					close(input);close(output);
					halt;
				end;
				if b[i]=0 then begin
					b[i]:=nxt;
					dye(i);
				end;
			end;
	end;
procedure makeans(c,v:longint);
	var
		i:longint;
	begin
		for i:=1 to n do
			if p[i]=c then begin
				if b[i]=v then begin
					inc(ans1[0]);
					ans1[ans1[0]]:=i;
				end
				else begin
					inc(ans2[0]);
					ans2[ans2[0]]:=i;
				end;
			end;
	end;
begin
	assign(input,'makegroup.in');reset(input);
	assign(output,'makegroup.out');rewrite(output);
	readln(n);
	for i:=1 to n do begin
		fillchar(pd,sizeof(pd),1);
		pd[i]:=false;
		read(k);
		while k>0 do begin
			pd[k]:=false;
			read(k);
		end;
		readln;
		for j:=1 to n do
			if pd[j] then begin
				a[i,j]:=true;
				a[j,i]:=true;
			end;
	end;
	c:=0;
	for i:=1 to n do
		if b[i]=0 then begin
			inc(c);
			b[i]:=1;
			dye(i);
		end;
	f[0,0]:=true;
	for i:=1 to c do
		for j:=-n to n do
			if f[i-1,j+q[i]] then begin
				f[i,j]:=true;
				l[i,j]:=true;
			end
			else if f[i-1,j-q[i]] then f[i,j]:=true;
	for i:=0 to n do
		if f[c,i] then begin
			k:=i;
			for j:=c downto 1 do
				if l[j,k] then begin
					makeans(j,2);
					inc(k,q[j]);
				end
				else begin
					makeans(j,1);
					dec(k,q[j]);
				end;
			break;
		end
		else if f[c,-i] then begin
			k:=-i;
			for j:=c downto 1 do
				if l[j,k] then begin
					makeans(j,2);
					dec(k,q[j]);
				end
				else begin
					makeans(j,1);
					inc(k,q[j]);
				end;
			break;
		end;
	write(ans1[0],' ');
	for i:=1 to ans1[0] do write(ans1[i],' ');
	writeln;
	write(ans2[0],' ');
	for i:=1 to ans2[0] do write(ans2[i],' ');
	writeln;
	close(input);close(output);
end.

  总结:这次比赛我得到了130分,被樊神完爆了……T1T3的方案咋一看很简单,但想出来却很不容易。比赛中我好像没犯什么重大错误,因此得分比较正常。还是要加油啊,总是被低年级的大神虐也不是什么好事对不对^_^

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值