Picture 题目大意 IOI 1998
求n (<=5000)个矩形 覆盖的图形 的周长(包括洞), 坐标范围[-10000,10000]
题解 一眼离散化+2维线段树,但仔细一想 空间不太够,时间勉强接受
然后目测可能1维线段树+扫描线了?
然后 竟然 裸的扫描线可以过,如下面代码
总数量级上来讲,输入O(n)
,排序O(n log n)
,扫描过程O(sum(len周长))
约5000*20000*4
的上限[ 不过USACO给过了,
所以还是线段树好?
从实现来讲,把矩形拆分成x和y方向,靠计算每个块的累计层数 来判断边界
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <bits/stdc++.h> #define rep(i,a,n) for (int i=a;i<n;i++) #define per(i,a,n) for (int i=n-1;i>=a;i--) using namespace std;const string filename = "picture" ;void usefile () { freopen ((filename+".in" ).c_str (),"r" ,stdin); freopen ((filename+".out" ).c_str (),"w" ,stdout); } int N,ans=0 ;tuple<int ,bool ,int ,int > Lx[10010 ],Ly[10010 ]; int level[20010 ];void Scan (tuple<int ,bool ,int ,int > *L) { sort (L,L+N); rep (i,0 ,20001 ) level[i]=0 ; rep (i,0 ,N){ rep (j,get <2 >(L[i]),get <3 >(L[i])){ if (!get <1 >(L[i])){ ans += level[j+10000 ]==0 ; level[j+10000 ]++; } else { level[j+10000 ]--; ans += level[j+10000 ]==0 ; } } } } int main () { usefile (); scanf ("%d" ,&N); rep (i,0 ,N){ int x1,x2,y1,y2; scanf ("%d %d %d %d" ,&x1,&y1,&x2,&y2); Lx[i*2 ] = {y1,false ,x1,x2}; Lx[i*2 +1 ] = {y2,true ,x1,x2}; Ly[i*2 ] = {x1,false ,y1,y2}; Ly[i*2 +1 ] = {x2,true ,y1,y2}; } N*=2 ; Scan (Lx); Scan (Ly); printf ("%d\n" ,ans); return 0 ; }
Hidden Password ACM South Eastern Europe – 2003
题目大意 字符串L(长度<=100’000
)
求 以该字符串,平移后的, 所有字符串中 字典序最小的字符串的首字母在原字符串中的下标
如cba
所有字符串 acb bac cab, (排序后),第一个字符串首字母对应原来字符串位置为2 (从0计数)
题解 枚举!
这样 我们首先找到最小字母 扫一遍O(n)
然后找到这些开始的坐标
逐步增加长度
那么我们由递归关系,保证每次增加长度后,当前还剩余的坐标只有最小的留下,
因此当增加长度到l
时,我们 的维持的起始坐标是 整个字符串里,长度从1
到l
都是字典序最小的
那么我们有两种长度改变方式
+1
假设所有点按照长度扩充 都不属于当前维护的点,那么,长度加1,保留,增加字符最小的点
例子 abcabdzzzabc
初始所有a
的坐标[0,3,9]
,长度1
扩充,扩充目标分别为[1,4,10]
,都不是当前维护的点([0,3,9]
)
所以比较元素,全为b,长度=1+1=2
接下来,扩充目标为[2,5,11]
,也都不是维护的点
比较字符,两个abc
,一个abd
,所以维护的点变为[0,9]
,长度变为=2+1=3
再扩充,扩充目标为[3,0]
,注意到0
是我们当前维护的[0,9]
中的元素,所以不采取+1
的方案
倍增
假设字符aabbabbb,
那么在找完最小字符后,起始坐标还剩下[0,1,4]
,一旦发现任意一个扩充的下一步([1,2,5]
) 是一个维持的点,那么长度翻倍,后一个点删除,在这种情况下,扩充的位置不是最小坐标的点直接移除。
因为我们维持的点 == 从1到该长度下,每个长度 都是字典序最小的,所以没有在维护中的点,都是非字典序最小的,所以 可以倍增
删除右边的点是因为 扩充右边的维护点的字典序一定大于等于 左边的点,等于的情况由判环处理
如上在一次扩充后发现0
的下一个扩充是1
,而1
是我们维持着的点,所以长度=1*2
,1
点删除,4
扩充是5
,那么5
没有被维持,所以4
点也被删除,综上最后剩下0
以上描述存在的问题:起始点是哪个点?
假设字符串 aaazzaaza
,
显然在初始操作后 需要维护的点有[0,1,2,5,6,8]
注意到,如果从左向右来处理,按照上面的算法会 变成[0,6,8???]
,而实际 从环的性质来看,期望的应该是得到[1,6,8]
,也就是8
位置的看做[8,0,1,2]
这一段的起始点。
这里加一个父节点的查找,找到环意义上该点所对应的最左的点即可,在下方函数看的话就是circle
,
同时,circle
这里如果发现,整个保存的点构成了环,那么也就是 这些点仅对于环上字典序的等价了,根据题目期望这种情况下最小index,就取出即是答案
空间复杂度,emmmm没啥好说,看变量即可,维持在O(n)
时间复杂度,
每一次倍增会让点数至少除以2,因为一个点要留下来,那么首先它的扩展点要在原来的维护里,并且下一次维护需要消失,所以每次要留一个点,就一定要删一个点,还有的点不会被留下,所以留下的一定小于等于上一次的一半
O(n+n/2+n/4) = O(2n) = O(n)
考虑对任意长度,都是执行+1,那么每次能执行+1的限度为
sum(n*(1+1/2+...1/n))
众所周知这是一个无穷级数,所以时间复杂度无穷大
大自也是O(12n)=O(n)
的复杂度,
下面就是实际上,是这两种穿插 ,那么一定小于等于O(2n+12n)=O(n)
, (数学的习惯懒得分类讨论不等的情况 能用就行,所以留一个等于)
综上 时间空间均满足
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 char s[100010 ];int sz=0 ;bool pos[100010 ];vector<int >p; vector<int >q; int L;bool circle (int idx,int len,int &newidx) { newidx = (idx+L-len)%L; while (newidx != idx){ if (!pos[newidx]){ (newidx+=len)%=L; return false ; }else { newidx = (newidx+L-len)%L; } } while (newidx - L > 0 ){ newidx -= L; } printf ("%d\n" ,newidx); return true ; } int main () { usefile (); scanf ("%d" ,&L); while (sz<L){ scanf ("%s" ,s+sz); sz+=strlen (s+sz); } char minch = s[0 ]; rep (i,1 ,L){ minch = min (minch,s[i]); } rep (i,0 ,L){ if (s[i] == minch){ p.push_back (i); pos[i]=true ; } } int l = 1 ; while (p.size () > 1 ){ int state = 0 ; minch = s[(p[0 ]+l)%L]; for (auto idx : p){ if (pos[(idx+l)%L] == true ){ state = 1 ; break ; } minch = min (minch,s[(idx+l)%L]); } if (state == 0 ){ q.clear (); for (auto idx:p){ if (!pos[idx])continue ; if (s[(idx+l)%L] == minch){ q.push_back (idx); }else { pos[idx]=false ; } } p=q; l++; }else { q.clear (); int startidx ; int ret = circle (p[0 ],l,startidx); if (ret){ return 0 ; } int pidx = 0 ; for (pidx=0 ;pidx<p.size ();pidx++){ if (p[pidx] == startidx){ break ; } } rep (i,pidx,p.size ()){ int idx = p[i]; if (!pos[idx])continue ; if (pos[(idx+l)%L]){ q.push_back (idx); pos[(idx+l)%L] = false ; }else { pos[idx]=false ; } } rep (i,0 ,pidx){ int idx = p[i]; if (!pos[idx])continue ; if (pos[(idx+l)%L]){ q.push_back (idx); pos[(idx+l)%L] = false ; }else { pos[idx]=false ; } } p=q; l*=2 ; } } printf ("%d\n" ,p[0 ]); return 0 ; }
Twofive 题目大意 IOI 2001
A到Y
构成的排列,满足 把这25
个字母排成 5*5
矩阵后 每行每列,单调递增,则为合法的
所有合法的排列,按照字典序 排序
请编写 字典序序号 到字符串 和 字符串反向转换为字典序序号的程序
尝试思路 看数据量,我自己也估计不到实际大小于是先写了一个打表,(上界 是25!
但是有限制所以不知道是否会降低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <bits/stdc++.h> #define rep(i,a,n) for (int i=a;i<n;i++) #define per(i,a,n) for (int i=n-1;i>=a;i--) using namespace std;const string filename = "twofive" ;void usefile () { freopen ((filename+".in" ).c_str (),"r" ,stdin); freopen ((filename+".out" ).c_str (),"w" ,stdout); } int chars[30 ];int vis[30 ];int cnt = 0 ;void print () { cout<<endl; rep (i,0 ,5 ){ rep (j,0 ,5 ){ cout<<char (chars[i*5 +j]+'a' )<<" " ; } cout<<endl; } } void gen (int idx) { if (idx%5 ==0 ){ rep (i,0 ,25 ){ if (vis[i] == 0 ){ vis[i]=1 ; chars[idx] = i; gen (idx+1 ); vis[i]=0 ; return ; } } } if (idx == 24 ){ cnt++; chars[24 ]=24 ; print (); return ; } int sti = chars[idx-1 ]; if (idx>5 ){ sti=min (sti,chars[idx-5 ]); } rep (i,sti+1 ,26 -(5 -idx%5 )*(5 -idx/5 )){ if (vis[i])continue ; vis[i]=1 ; chars[idx] = i; gen (idx+1 ); vis[i]=0 ; } } int main () { gen (0 ); cout<<cnt<<endl; return 0 ; }
跑了一下大概
1 2 3 4 5 6 7951237 a b c d e f g h l n i m k q w j p o r u s t v x y
以上的改动才到i
,说明 数量级上,不可能期望打表了
接下来,注意到,如果我们有方法从数字序号转换到 对应的单词,那么 可以2分法 找到对应的单词
同理,如果我们找到 单词映射到序号的方法,那么2分(如果可以,因为这里二分单词似乎没那么好做)也能反过来找数字,所以分析上,任何一个问题 都可以解决对应的一个
还有个思路是,简单的排序,计算被删掉的个数,那么序列= 总排序-被删掉比它小的个数
题解 记忆化+dp 我们如果把数从小到大填入
那么显然,新插入的数在已经插入的数的行末,也在已经插入的数的列末,如
j
可插入的位置 为g右侧
或e右侧
所以 我们有dp
dp[i0][i1][i2][i3][i4]
表示 第0行i0
个,第1行i1
个数…第i4
行个数的情况下,剩余未填部分的期望个数
不考虑具体,仅考虑题目基本限制的情况下, 满足 ij >= i(j+1)
,因为我们按照顺序放数字,所以上面的行的个数不小于下一行
有转移方程
dp[i0][i1][i2][i3][i4] = dp[i0-1][...]+dp[...][i1-1][...]+dp[...][i2-1][...]+dp[...][i3-1][...]+dp[...][i4-1]
其中 如果在-1
时不满足 行之间个数的大小关系,那么 对应的dp直接为0
综上,我们有了在没有 具体求值下,能计算所有满足题目限制下的dp,时间复杂度 = O(空间*状态转移)=O(6^5*5)
,空间O(6^5)
求解 接下来是如何进行转换的问题求
因为所求的idx为实际的 合法twofive的字典序
那么我们可以按位逼近,//延伸阅读 BROP的按位枚举攻击方法
假设我们求的是
ADEFGBHIJKC...Y
, 那么 它 的字典序 = AB...的所有
+AC...的所有
+…
简单的说,如果一个前缀小于 要求的等长前缀,那么加上该前缀的所有个数,
如果该前缀等于要求的值的等长前缀,那么前缀长度+1
外层for前缀长度,中间for字母, 时间复杂度小于 O(25*25)
以上我们可以 从 字符串转化为index
相反
同样用逼近的方法,可以O(25*25)
时间复杂度内 index转化为 字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 #include <bits/stdc++.h> #define rep(i,a,n) for (int i=a;i<n;i++) #define per(i,a,n) for (int i=n-1;i>=a;i--) using namespace std;const string filename = "twofive" ;void usefile () { freopen ((filename+".in" ).c_str (),"r" ,stdin); freopen ((filename+".out" ).c_str (),"w" ,stdout); } char s[100 ]; char str[100 ]; int dp[6 ][6 ][6 ][6 ][6 ];int dfs (int a=0 , int b=0 , int c=0 , int d=0 , int e=0 , char ch='A' ) { if (ch > 'Y' ) return 1 ; int &ret = dp[a][b][c][d][e]; if (ret) return ret; int w = 5 ; int *v[6 ]={&w,&a,&b,&c,&d,&e}; rep (i,1 ,6 ){ int idx = *v[i]+(i-1 )*5 ; if (*v[i] < *v[i-1 ] && (s[idx] == 0 || s[idx] == ch)){ (*v[i])++; ret+=dfs (a,b,c,d,e,ch+1 ); (*v[i])--; } } return ret; } void index2word () { int n; scanf ("%d" ,&n); rep (i,0 ,25 ){ for (s[i] = 'A' ;; s[i]++) { memset (dp, 0 ,sizeof (dp)); int ret = dfs (); if (ret >= n) break ; n -= ret; } } printf ("%s\n" , s); } void word2index () { scanf ("%s" , str); int ans = 1 ; rep (i, 0 , 25 ) { for (s[i] = 'A' ; s[i] < str[i]; s[i]++) { memset (dp, 0 ,sizeof (dp)); ans += dfs (); } } printf ("%d\n" , ans); } int main () { usefile (); char c; cin >> c; if (c == 'N' ) { index2word (); } else if (c == 'W' ) { word2index (); } return 0 ; }
上面实现中 dp过程是按照字母顺序填写,由ch保证,所以在最外层枚举dp的时候,就直接从A 到Y 了