Monday, October 31, 2011

NOIP 2005 青蛙过河

首先想到的是简单的动态规划,时间复杂度是LM,M是t-s,转移公式是:
f[i] = min{f[i - j]} s<=j<=t i位置没有石子 = min{f[i - j]} + 1 s<=j<=t i位置有石子 可是,本题的L可以达到10^9,即使压缩了数组空间,时间也不允许
注意到题目的石子数量不到100个,因此,当L达到很大时,一定存在很长的一段或几段没有石子,在S不等于T的时候,青蛙在这里面怎么跳都不会踩到石头,如果可以把这些“空长条”去掉,那么实际需要计算的长度就可以大大降低。

如何确定空长条的范围呢?

题目有说s,t<=10。读者可以估计一下,本人认为长度大于100就可以看作空长条了。RQNOJ上有人专门对这个问题做了分析,有兴趣的读者可以去看一下。 需要注意的是,题目没有说明石头的位置是递增输入的,因此别忘了排序

排序后,就可以逐一检查两个石头之间的距离,如果大于100,就把后面的石头全部向前移动,使这一段变成刚好100.对于L,其实它只要是最后一个石头的位置+t就可以了。

然后可以利用上面的关系式进行DP,最后在f[最后一个石头的位置]~[最后一个石头的位置+t]中选取最小值输出即可。

不过,在测试的时候,发现使用上面的方法有两个数据不能通过。最后发现那两个数据的s=t。在这种情况下,空长条就不能被移除,而应之间判断每个石子的位置是否可以被s整除。

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int s,t,m;
long l;
int f[20000];
_Bool havestone[20000];
long stone[102];

int cmp(const void *a,const void *b)
{
    return (*(long *)a - *(long *)b);
}

int main()
{
    long i,j;
    long del;
    int min;
    scanf("%ld%d%d%d",&l,&s,&t,&m);
    for(i = 1;i <= m;i ++)   scanf("%ld",&stone[i]);
    stone[0] = 0;   //方便处理
    qsort(stone,m + 1,sizeof(long),cmp);
    if(s == t)
    {
        j = 0;
        for(i = 1;i <= m;i ++)
            if(stone[i] % s == 0)   j ++;
        printf("%ld\n",j);
        exit(0);
    }
    //压缩“空长段”
    for(i = 1;i <= m;i ++)
    {
        if(stone[i] - stone[i - 1] > 100)
        {
            del = stone[i] - stone[i - 1] - 100;
            for(j = i;j <= m;j ++)
            {
                stone[j] -= del;
            }
        }
    }
    if(stone[m] + 12 < ll = stone[m] + 12;
    for(i = 1;i <= m;i ++)
        havestone[stone[i]] = 1;
    //DP过程
    f[0] = 0;
    for(i = 1;i <= l + t;i ++)
    {
        min = INT_MAX;
        for(j = s;j <= t;j ++)
        {
            if((i - j) >= 0 && f[i - j] < minmin = f[i - j];
        }
        f[i] = min;
        if(havestone[i])    f[i] ++;
    }
    min = INT_MAX;
    for(i = stone[m];i <= l + t;i ++)
        if(f[i] < minmin = f[i];
    printf("%d\n",min);
    return 0;
}

Friday, October 21, 2011

NOIP 2000 方格取数

题目描述
设有N*N的方格图(N<=10,我们将其中的某些方格中填入正整数,而其他的方格中则放入数字0。 某人从图的左上角的A 点出发,可以向下行走,也可以向右走,直到到达右下角的B点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。
此人从A点到B点共走两次,试找出2条这样的路径,使得取得的数之和为最大。

输入格式
输入的第一行为一个整数N(表示N*N的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的0表示输入结束。

输出格式
只需输出一个整数,表示2条路径上取得的最大的和。

-----------------------------------------------------------------------------------------------------------------------------------

这道题是动态规划的代表题目,使用的是所谓的“双线递归”,即假设有两个人同时从左上角开始走,假设两个人走到的位置是(x1,y1)和(x2,y2),那么可以用f[x1][y1][x2][y2]表示此时可以获得的最大分值,转移方程为(map表示分值):
f[x1][y1][x2][y2]
= max{f[x1 - 1][y1][x2 - 1][y2],f[x1 - 1][y1][x2][y2 - 1],f[x1][y1 - 1][x2 - 1][y2],f[x1][y1 - 1][x2][y2 - 1]} + map[x1][y1] + map[x2][y2] 当(x1,y1)和(x2,y2)不重合
= max{f[x1 - 1][y1][x2 - 1][y2],f[x1 - 1][y1][x2][y2 - 1],f[x1][y1 - 1][x2 - 1][y2],f[x1][y1 - 1][x2][y2 - 1]} + map[x1][y1] 当(x1,y1)和(x2,y2)重合

最后输出f[n][n][n][n]即可。

假如在方格左上角的左边再放一个格子(0,0)作为起点,到达右下角走的步数一定是2n - 1,并且x = step - y。根据上面的方程,f只和上一步有关系,因此,简化状态表示为f[k][x1][x2],相应的转移方程可以参照上面写出。边界是f[0][0][0] = 0。

如果使用这种方法,还会发现f[k]只和f[k - 1]有关,因此可以采用滚动数组进一步优化空间。

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int n;
05 int map[11][11];
06 int f[25][15][15];
07 int di[4] = {1,1,0,0};
08 int dj[4] = {0,1,0,1};
09
10 int get_max(int a,int b)
11 {
12     if(a > b)   return a;
13     return b;
14 }
15
16 int main()
17 {
18     int a,b,c;
19     int i,j,k,t;
20     int max;
21     scanf("%d",&n);
22     while(1)
23     {
24         scanf("%d%d%d",&a,&b,&c);
25         if(a == 0 && b == 0 && c == 0break;
26         map[a][b] = c;
27     }
28     f[0][0][0] = 0;
29     for(k = 1;k <= 2 * n;k ++)
30         for(i = 1;i <= k;i ++)
31             for(j = 1;j <= k;j ++)
32             {
33                 max = 0;
34                 for(t = 0;t < 4;t ++)
35                     max = get_max(max,f[k - 1][i - di[t]][j - dj[t]]);
36                 if(i == jf[k][i][j] = max + map[i][k - i + 1];
37                 else f[k][i][j] = max + map[i][k - i + 1] + map[j][k - j + 1];
38                 //printf("%d %d %d %d %d\n",i,k + 1 - i,j,k + 1 - j,f[k][i][j]);
39             }
40     printf("%d\n",f[2 * n - 1][n][n]);
41     return 0;
42 }

Wednesday, October 19, 2011

NOIP 2000 解题报告

进制转换
其实负数进制和正数的解决方法是类似的,需要注意的是计算机计算余数符号取决于被除数,因此如果为负数则要再加上除数使其变为正数。还需要注意的是,整除不能再使用num /= base了,因为这种方法不适用于负数,取而代之的是num = (num - 余数(正的)) / base.

贴一小段函数

01 void process(int n,int b)
02 {
03     int i;
04     top = 0;
05     printf("%d = ",n);
06     while(n != 0)
07     {
08         stack[top] = n % b;
09         if(stack[top] < 0stack[top] -= b;
10         n = (n - stack[top]) / b;
11         //it is different from 'n /= b' which only works for positive integer;
12         top ++;
13     }
14     for(i = top - 1;i >= 0;i --)    printf("%c",map[stack[i]]);
15     //map stores the corresponding alphabet of number i;
16     printf(" (base %d)\n",b);
17 }

最大乘积

本题是矩阵连乘的改编,可以参考算法导论。
状态表示:f[n][i][j]在第i个字符和第j个字符中放置n个乘号可以获得的最大乘积。
转移方程:f[n][i][j] = max{f[n - k][i][t] * f[n - k - 1][t + 1][j]}
                    0 <= k <= n - 1 i <= t <= j - 1
f[K][0][N - 1]即为最终结果。

单词接龙
由于单词数量不多,考虑搜索。先计算每个单词直接是否可以连接和重合部分的长度,然后DFS找最长串即可。
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <string.h>
04
05 char str[21][50];
06 int len[21];
07 int adj[21][21];
08 int used[21];
09 int n;
10 int ans = 0;
11 char begin;
12
13 int calc_dist(int a,int b)
14 {
15     int i,l;
16     if(len[a] > len[b]) l = len[b];
17     else l = len[a];
18     for(i = 1;i < l;i ++)
19     {
20         if(strncmp(str[a] + len[a] - i,str[b],i) == 0return i;
21     }
22     return 0;
23 }
24
25 void dfs(int now,int tot)
26 {
27     int i;
28     if(tot > ans)
29         ans = tot;
30     for(i = 0;i < n;i ++)
31     {
32         if(used[i] < 2 && adj[now][i] > 0)
33         {
34             used[i] ++;
35             dfs(i,tot + len[i] - adj[now][i]);
36             used[i] --;
37         }
38     }
39 }
40
41 int main()
42 {
43     int i,j;
44     scanf("%d",&n);
45     for(i = 0;i < n;i ++)
46     {
47         scanf("%s\n",str[i]);
48         len[i] = strlen(str[i]);
49     }
50     scanf("%c",&begin);
51
52     for(i = 0;i < n;i ++)
53         for(j = 0;j < n;j ++)
54             adj[i][j] = calc_dist(i,j);
55
56     for(i = 0;i < n;i ++)
57     {
58         if(str[i][0] == begin)
59         {
60             used[i] = 1;
61             dfs(i,len[i]);
62             used[i] = 0;
63         }
64     }
65     printf("%d\n",ans);
66     return 0;
67 }

Tuesday, October 18, 2011

NOIP 2011 乌龟棋 解题报告

从数据规模可以看出本题用暴力搜索是行不通的,考虑使用动态规划。我折腾了一个多小时,竟然没有想出一个合适的状态表示。最后无奈地看了题解,程序很简单。

f[a][b][c][d]表示使用a个前进1的卡片,b个前进2的卡片……可以获得的最大分值

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int count[5];   //记录每种卡片出现次数
05 int value[360];  //每个位置的分值
06 int M,N;
07
08 long f[41][41][41][41];
09
10 long get_max(long a,long b)
11 {
12     if(a > b)   return a;
13     return b;
14 }
15
16 void solve()
17 {
18     int i,j,k,t;
19     int pos;
20     f[0][0][0][0] = value[0];
21     for(i = 0;i <= count[1];i ++)
22         for(j = 0;j <= count[2];j ++)
23             for(k = 0;k <= count[3];k ++)
24                 for(t = 0;t <= count[4];t ++)
25                 {
26                     pos = i + 2 * j + 3 * k + 4 * t//乌龟的位置
27                     f[i][j][k][t + 1] = get_max(f[i][j][k][t + 1],f[i][j][k][t] + value[pos + 4]);
28                     f[i][j][k + 1][t] = get_max(f[i][j][k + 1][t],f[i][j][k][t] + value[pos + 3]);
29                     f[i][j + 1][k][t] = get_max(f[i][j + 1][k][t],f[i][j][k][t] + value[pos + 2]);
30                     f[i + 1][j][k][t] = get_max(f[i + 1][j][k][t],f[i][j][k][t] + value[pos + 1]);
31                 }
32 }
33
34 int main()
35 {
36     scanf("%d%d",&N,&M);
37     int i,step;
38     for(i = 0;i < N;i ++)
39         scanf("%d",&value[i]);
40     for(i = 0;i < M;i ++)
41     {
42         scanf("%d",&step);
43         count[step] ++;
44     }
45     solve();
46     printf("%ld\n",f[count[1]][count[2]][count[3]][count[4]]);
47     return 0;
48 }

虽然简单,不过这种状态表示方法依然值得分析。

可行吗?
对于一个给定的棋盘和卡片,由于卡片全部都要使用,所以可以获得的最大分值一定是唯一的。那么,获得最大分值的方案也是可以确定的(虽然说方案可能有多解情况),所以这种状态表示是唯一的。
假如我们知道某一时刻使用过的每一种卡片的数量,那么当前乌龟在棋盘上的位置就是确定的,然后我们对剩下的卡片进行选择,分别计算出使用某个卡片后的可以达到的最大分值。

题目条件的暗示
题目中有说明卡片只有4种,并且要全部使用(我最初一直不知道这个条件拿来干什么的),每个卡片的数量不超过40.通过题目条件的暗示,也容易想到这种解法。

类似的题目
交换游戏,参见http://tenpages.blogcn.com/archives/72

Sunday, October 16, 2011

Stringsobits 解题报告

刚开始想到位运算,Matrix67有说过一个求某个二进制数中1的个数的方法。那么可以直接枚举,然后判断1的个数是否大于L,直到求出符合条件第I个数。但是由于最后结果最大可以到LONG_MAX,所以即使用位运算也会超时。

既然filter不可以,那么考虑generator。

通过打出前几个自然数的二进制:

000000
000001
000010
000011
000100
000101
000110
000111
001000
001001
001010
001011
001100
001101
001110
001111
010000
010001
010010
010011
010100
010101
010110
010111
011000
011001
011010
011011
011100
011101
011110
011111

可以发现,如果用f[i][j]表示长度为i的二进制串恰好含有j个1的串的个数,可以得到如下关系:
f[i][j] = f[i - 1][j] + f[i - 1][j - 1]
f[i][0] = 1
f[i][i] = 1

即可用下列语句递推

1 for(i = 1;i <= N;i ++)
2 {
3     f[i][i] = f[i][0] = 1;
4     for(j = 1;j < i;j ++)
5         f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
6 }

因为题目要求的是1的个数不大于L的串,因此把f[i][j]转换为长度为i的串出现1的次数小于j的个数

1 for(i = 1;i <= N;i ++)
2     for(j = 1;j <= N;j ++)
3         f[i][j] += f[i][j - 1];

现在所需要做的就是构造出答案二进制串,我在这个问题上卡了很久。

01 for(i = N;i > 0;i --//从高位向低位推
02 {
03     if(f[i - 1][L] < I) //此处不能使用<=,我就是在这里纠结好久的,为什么可以观察上面的二进制串
04     {
05         printf("1");
06         I -= f[i - 1][L];
07         L --//既然该位有了一个1,剩下的位置的1的个数的上限就要-1
08     }
09     else printf("0");
10 }

全部代码:

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 long long N,L,I//注意使用long long型,否则第10个数据过不了
05 unsigned long long f[32][32];
06
07 void solve()
08 {
09     int i,j;
10     for(i = 0;i <= N;i ++//这个是上面代码所没有的,应该也算递归边界吧,其实也是为重构二进制串做边界
11         f[0][i] = 1;
12     for(i = 1;i <= N;i ++)
13     {
14         f[i][i] = f[i][0] = 1;
15         for(j = 1;j < i;j ++)
16             f[i][j] = f[i - 1][j - 1] + f[i - 1][j];  //这个其实就是个杨辉三角
17     }
18     for(i = 1;i <= N;i ++)
19         for(j = 1;j <= N;j ++)
20             f[i][j] += f[i][j - 1];
21     for(i = N;i > 0;i --)
22     {
23         if(f[i - 1][L] < I)
24         {
25             printf("1");
26             I -= f[i - 1][L];
27             L --;
28         }
29         else printf("0");
30     }
31     printf("\n");
32     return;
33 }
34
35 int main()
36 {
37     freopen("kimbits.in","r",stdin);
38     freopen("kimbits.out","w",stdout);
39     scanf("%Ld%Ld%Ld",&N,&L,&I);
40     solve();
41     fclose(stdout);
42     fclose(stdin);
43     return 0;
44 }

Friday, October 7, 2011

Factorials 解题报告

题意:求出n!最右侧的非零数字。

我的想法:因为0x任何数均为0,那么每循环一次,就把末尾多余的0去掉。

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 long last(long i)
05 {
06     while(i % 10 == 0)
07         i /= 10;   //删除末尾0
08     return i % 10000//防止溢出
09 }
10
11 int main()
12 {
13     long ans,i,n;
14     freopen("fact4.in","r",stdin);
15     freopen("fact4.out","w",stdout);
16     scanf("%ld",&n);
17     for(i = ans = 1;i <= n;i ++)
18     {
19         ans *= i;
20         ans = last(ans);
21     }
22     printf("%ld\n",ans % 10);
23     fclose(stdin);fclose(stdout);
24     return 0;
25 }

官方题解给出了一个适用范围更广的解法:

The insight for this problem is that 0's at the end of a number come from it being divisible by 10, or equivalently, by 2 and 5. Furthermore, there are always more 2s than 5s in a factorial.

To get the last digit of the factorial, we can run a loop to calculate it modulo 10, as long as we don't include any 2s or 5s in the product. Of course, we want to exclude the same number of 2s and 5s, so that all we're really doing is ignoring 10s. So after the loop, we need to multiply in any extra 2s that didn't have 5s to cancel them out.