//---------------------------------------------------------------------------- // ypSched.c // //---------------------------------------------------------------------------- // Change history // // 28-dec-99 Originally written by J. Henckel // 7-dec-01 added room numbers & descriptions // 1-dec-02 allow new input format "name",n,n,n,n // 6-dec-02 allow prereq and coreq classes #include #include #include #include #include //---------------------------------------------------------------------------- // terminology // // a period is an time spot, like "Wednesday afternoon". each period has // several classes (in parallel). each class has a topic and a size. each // student is enrolled in exactly one class per period. each student has // preferences based on the topic. a topic may be offered during several // periods. the num_seat is the total number of seats available for a // particular topic. // // NOTE: do not have more than one class in the same period on the // same topic, or else things might not work. If you do have two classes // like that, then just add their size together and call it one big class. // // A word of explanation about prereqs. Instead of making one topic prereq // another, I decided to make one class prereq another. A class is a // topic at a specific period. It's less flexible, but lots easier for me // to implement. The (prep,prec) pair points from each class to its // prereq class. The (prap,prac) is the backwards pointer for the same // relation (it is needed for unenroll to remove dependents of a class). // If you wanna make co-requisite classes, you can just make them prereq // each other in a circle. //---------------------------------------------------------------------------- // types typedef struct { char *name; int *enroll; // one per period int happiness; int *pref; // one per topic int *seen; // one per topic int num_enroll; } Student; typedef struct { char *name; int num_period; int num_seat; int sum_pref; } Topic; typedef struct // all these int* are base-1 arrays. { char *name; // such as "Wednesday morning" int *topic; // list of topics int *size; // how many seats are available int *cap; // total number of seats int *room; // room number (for output only) int *prec; // prerequisite class (base 1) int *prep; // prerequisite period (if prec nonzero)7 int *prac; // post-requisite class (base 1) int *prap; // post-requisite period (if prac nonzero) } Period; //---------------------------------------------------------------------------- // global data // // note: the arrays in each period are base 1 (zeroth entry not used) // because the enroll[] array uses zero to indicate "not enrolled". int num_student; Student *student; int num_topic; Topic *topic; int num_period; Period *period; int max_class; // max num classes per period PLUS ONE double w1 = 5.0; // weight for flexibility vs demand int prevent_repeat = 0; // no hard constraint for non-repeat int debug = 1; // medium level output int swlim = 100; int quick = 0; // only binary swaps in optimizer int evenly = 1; int export = 0; int *perm; // random order of students //---------------------------------------------------------------------------- // random number int Random(int m) { return rand() % m; } //---------------------------------------------------------------------------- // string duplicate char* StringDup(char* s) { char *t; int n; if (!s) return NULL; n = strlen(s) + 1; t = malloc(n); memcpy(t,s,n); while (n-- && t[n]<=' ') // remove trailing whitespace t[n]=0; return t; } //---------------------------------------------------------------------------- // read data file // // FILE FORMAT // topics #topic // // periods #period, max#classes per period // // students #students // int ReadFile(char *filename) { int i,j,rc; char buf[200]; FILE *f; if (debug>1) printf("open file %s\n",filename); f = fopen(filename, "r"); if (!f || ferror(f)) return printf("Unable to read from data file\n"); //----------------------- // topics while (fgets(buf, sizeof buf, f) && strncmp(buf,"topics ",7)); sscanf(buf+7, "%d", &num_topic); topic = calloc(num_topic, sizeof *topic); if (debug>1) printf("reading %d topics\n",num_topic); for (i=0; i< num_topic; ++i) { fgets(buf, sizeof buf, f); topic[i].name = StringDup(buf); #if 0 j = topic[i].name[strlen(topic[i].name)-1]; if (j=='=' || j=='+') { topic[i].name[strlen(topic[i].name)-1] = 0; topic[i].prereq = i - 1; // pre-requisite if (j=='=') // co-requisites { topic[i].prereq = topic[i-1].prereq; topic[i-1].prereq = i; } } else topic[i].prereq = i; // self requisite #endif if (debug>1) printf("topic %d = %s\n",i,topic[i].name); topic[i].sum_pref = 0; } //----------------------- // periods while (fgets(buf, sizeof buf, f) && strncmp(buf,"periods ",8)); sscanf(buf+8, "%d %d", &num_period, &max_class); period = calloc(num_period, sizeof *period); ++max_class; // make room for base-1 arrays if (debug>1) printf("reading %d periods\n",num_period); for (i=0; i< num_period; ++i) { fgets(buf, sizeof buf, f); period[i].name = StringDup(buf); if (debug>1) printf("period %d = %s\n",i,period[i].name); period[i].topic = calloc(max_class, sizeof(int)); period[i].size = calloc(max_class, sizeof(int)); period[i].cap = calloc(max_class, sizeof(int)); period[i].room = calloc(max_class, sizeof(int)); period[i].prec = calloc(max_class, sizeof(int)); period[i].prep = calloc(max_class, sizeof(int)); period[i].prac = calloc(max_class, sizeof(int)); period[i].prap = calloc(max_class, sizeof(int)); for (j=1; j 0 && rc != ',') // if not a comma put it back ungetc(rc,f); period[i].cap[j] = period[i].size[j]; if (period[i].size[j] == 0) period[i].topic[j] = -1; // flag a dummy class } } //----------------------- // prereqs rc = 0; while (fgets(buf, sizeof buf, f)) { int p1,c1,p2,c2; if (!strncmp(buf,"students ",9)) break; if (!rc && !strncmp(buf,"prereqs",7)) { rc = 1; if (debug>1) printf("reading prereqs\n"); continue; } if (4 != sscanf(buf, "%d %d, %d %d", &p1, &c1, &p2, &c2)) { if (strlen(buf) > 4) printf("ERROR unable to parse prereq: %s\n",buf); continue; } if (debug>1) printf("%s (%d. %.10s) prereq of %s (%d. %.10s)\n", period[p1].name, c1, topic[period[p1].topic[c1+1]].name, period[p2].name, c2, topic[period[p2].topic[c2+1]].name); c1++; c2++; if (p1 < 0 || p1 >= num_period || p2 < 0 || p2 >= num_period || c1 < 1 || c1 >= max_class || c2 < 1 || c2 >= max_class) { printf("ERROR prereq is not correct ==> %s\n",buf); continue; } period[p1].prac[c1] = c2; period[p1].prap[c1] = p2; // (p1,c1) is prereq of (p2,c2) period[p2].prec[c2] = c1; period[p2].prep[c2] = p1; } //----------------------- // students // while (fgets(buf, sizeof buf, f) && strncmp(buf,"students ",9)); sscanf(buf+9, "%d", &num_student); student = calloc(num_student, sizeof *student); if (debug>1) printf("reading %d students\n",num_student); for (i=0; i< num_student; ++i) { buf[0] = rc = fgetc(f); if (rc=='"') // new format "name",n,n,n,n fscanf(f,"%[^\"]\",",buf); else fgets(buf+1, sizeof buf, f); student[i].name = StringDup(buf); if (debug>1) printf("student %d = %s\n",i,student[i].name); student[i].pref = calloc(num_topic, sizeof(int)); student[i].seen = calloc(num_topic, sizeof(int)); student[i].enroll = calloc(num_period, sizeof(int)); student[i].num_enroll = 0; for (j=0; j1) printf(" %d",student[i].pref[j]); } if (debug>1) printf("\n"); } rc = ferror(f); if (debug>1) printf("closing file %s rc=%d\n",filename,rc); fclose(f); return rc; } //---------------------------------------------------------------------------- // run this after reading the data file void FinishInit(void) { int i,j,t,n; perm = calloc(num_student, sizeof *student); for (i=0; i< num_student; ++i) perm[i] = i; for (i=0; i< num_student; ++i) for (j=0; j< num_topic; ++j) topic[j].sum_pref += student[i].pref[j]; if (debug) printf("\n"); for (i=0; i< num_period; ++i) { n = 0; for (j=1; j< max_class; ++j) { if (period[i].cap[j] > 0) { n += period[i].cap[j]; t = period[i].topic[j]; assert(t >= 0 && t < num_topic); topic[t].num_period += 1; topic[t].num_seat += period[i].cap[j]; } } if (debug) printf(" %d seats on %s\n", n, period[i].name); } if (debug) { printf("\n"); for (j=0; j< num_topic; ++j) printf(" %d seats in %d. %s\n", topic[j].num_seat, j, topic[j].name); } } //---------------------------------------------------------------------------- // debug tool to check for prereqs int checkprereq() { int i, p, c, c2, t=0; for (i=0; i= num_period) i=0; // Can't go to two classes at once if (student[s].enroll[i] > 0) continue; for (j=1; j 0 && period[i].topic[j] == t) { *p = i; *c = j; return 1; } } return 0; } //---------------------------------------------------------------------------- // UnEnroll student s in period p, class c. void UnEnroll(int s, int p, int c, int x) { int t = period[p].topic[c]; if (debug>2) printf("%.*sunenroll %d %d %d\n",x,"........",s,p,c); assert(student[s].enroll[p] == c); assert(student[s].num_enroll > 0); assert(student[s].seen[t] > 0); student[s].enroll[p] = 0; student[s].num_enroll -= 1; student[s].happiness -= student[s].pref[t]; student[s].seen[t] -= 1; period[p].size[c] += 1; topic[t].num_seat += 1; topic[t].sum_pref += student[s].pref[t]; if (period[p].size[c] == 1) topic[t].num_period += 1; // test for postreq (a class that prereq's this one) t = c; c = period[p].prac[t]; p = period[p].prap[t]; t = period[p].topic[c]; if (c && student[s].enroll[p]==c) UnEnroll(s, p, c,x+1); } //---------------------------------------------------------------------------- // Enroll student s in period p, class c. // // return 1=success, 0=some prereqs not satisfied. int Enroll(int s, int p, int c, int x) { int t = period[p].topic[c]; if (debug>2) printf("%.*senroll %d %d %d\n",x,"........",s,p,c); assert(student[s].enroll[p] == 0); student[s].enroll[p] = c; student[s].num_enroll += 1; student[s].happiness += student[s].pref[t]; student[s].seen[t] += 1; period[p].size[c] -= 1; topic[t].num_seat -= 1; topic[t].sum_pref -= student[s].pref[t]; // if took last seat... if (period[p].size[c] == 0) topic[t].num_period -= 1; // test for prereq t = c; c = period[p].prec[t]; p = period[p].prep[t]; t = period[p].topic[c]; if (c && student[s].enroll[p] != c) // prereq is required { assert(student[s].enroll[period[p].prap[c]]==period[p].prac[c]); if (student[s].enroll[p]) UnEnroll(s,p,student[s].enroll[p],x+1); assert(!student[s].enroll[p]); Enroll(s, p, c, x+1); assert(student[s].enroll[p]==c); assert(student[s].seen[t]); assert(student[s].enroll[period[p].prap[c]]==period[p].prac[c]); } return 1; } //---------------------------------------------------------------------------- // enroll a student in whatever seems best for them in given period. // return the class number in the period. student must be unenrolled. int EnrollBest(int s, int p) { int c, t, h, bh, bc=0; if (debug>2) printf("enrollbest %d %d \n",s,p); assert(student[s].enroll[p] == 0); for (c = 1; c < max_class; ++c) { t = period[p].topic[c]; h = -10*student[s].seen[t] + student[s].pref[t]; if (period[p].size[c] < 1) h -= 100*(1 - period[p].size[c]); // penalize too full classes if (h > bh || c==1) { bh = h; bc = c; } } if (bc) Enroll(s, p, bc,0); return bc; } //---------------------------------------------------------------------------- // pick the topic with least num_period and highest demand, and // assign one student to it. repeat until everyone reaches // the enrollment limit. void Assignment(int limit) { int i,t,s,p,c,y; double d, best_d; while (1) { t = -1; for (i=0; i best_d) { t = i; best_d = d; } } if (t == -1) // no more seats in any topic break; s = -1; i = Random(num_student); for (y=0; y= num_student) i=0; if (student[i].num_enroll >= limit) continue; // This is the student heuristic.... d = student[i].seen[t] ? -100 : student[i].pref[t]; if (s < 0 || d > best_d || d == best_d && student[i].happiness < student[s].happiness) { if (FindClass(i,t,&p,&c)) { s = i; best_d = d; } } } // The following makes the "no repeat topic" a hard constraint if (best_d == -100 && prevent_repeat) s = -1; // test if no more students can take this topic. This only happens // if the number of seats in some period exceeds the number of students. if (s == -1) topic[t].num_seat = 0; else Enroll(s,p,c,0); if (debug>2) checkprereq(); // This is for debugging } } //---------------------------------------------------------------------------- // This is used to accumulate the effect of a swap, h is the change in // happiness, and e is change in square of happiness (which is used as a // measure of fairness. #define DELTA_he(s1,t1,t2) \ h += student[s1].pref[t2] - student[s1].pref[t1]; \ e += (student[s1].pref[t2] - student[s1].pref[t1])* \ (student[s1].pref[t2] + student[s1].pref[t1]) //---------------------------------------------------------------------------- // this makes one pass over every period and pair of students. // the students are swapped if it improves the total happiness. void SearchAndSwap(void) { int p,h,e, i1, i2, i3; int s1, s2, s3, c1, c2, c3, t1, t2, t3; for (p=0; p 0) { t1 = period[p].topic[c1]; t2 = period[p].topic[c2]; h = student[s1].pref[t2] - student[s1].pref[t1]; if (!student[s1].seen[t2] && (h > 0 || student[s1].seen[t1] > 1 || evenly && h >= 0 && period[p].size[c1]+1 < period[p].size[c2])) { UnEnroll(s1,p,c1,0); Enroll(s1,p,c2,0); c1 = c2; } } // Check for swap with other student for (i2=i1 + 1; i2 0 || h==0 && e < 0 || student[s1].seen[t1] > 1 || student[s2].seen[t2] > 1) { // If is doesn't cause a conflict, swap 'em if (!student[s1].seen[t2] && !student[s2].seen[t1]) { UnEnroll(s1,p,c1,0); UnEnroll(s2,p,c2,0); Enroll(s1,p,c2,0); Enroll(s2,p,c1,0); continue; } } if (quick) continue; // If the two way swap was not feasable, try a three way swap for (i3=i2 + 1; i3 0 || h==0 && e < 0 || student[s1].seen[t1] > 1 || student[s2].seen[t2] > 1 || student[s3].seen[t3] > 1) { // If is doesn't cause a conflict, swap 'em if (!student[s1].seen[t2] && !student[s2].seen[t3] && !student[s3].seen[t1]) { UnEnroll(s1,p,c1,0); UnEnroll(s2,p,c2,0); UnEnroll(s3,p,c3,0); Enroll(s1,p,c2,0); Enroll(s2,p,c3,0); Enroll(s3,p,c1,0); break; } } // Try a reverse 3-way swap h = e = 0; DELTA_he(s1,t1,t3); DELTA_he(s2,t2,t1); DELTA_he(s3,t3,t2); if (h > 0 || h==0 && e < 0 || student[s1].seen[t1] > 1 || student[s2].seen[t2] > 1 || student[s3].seen[t3] > 1) { // If is doesn't cause a conflict, swap 'em if (!student[s1].seen[t3] && !student[s2].seen[t1] && !student[s3].seen[t2]) { UnEnroll(s1,p,c1,0); UnEnroll(s2,p,c2,0); UnEnroll(s3,p,c3,0); Enroll(s1,p,c3,0); Enroll(s2,p,c1,0); Enroll(s3,p,c2,0); break; } } // Try a four way swap... ha ha, just kidding } } } } // Kick out people from overfilled classes. for (p=0; p 1); th += h; if (!i || mh <= h) { mh = h; mi = i; } if (!i || nh >= h) { nh = h; ni = i; } hd[h<20?h:19]++; // randomize the permutation (ordering) of students j = Random(num_student - i) + i; h = perm[j]; perm[j] = perm[i]; perm[i] = h; // swap // check for missing class or missing prereq for (p=0; p1) { printf("minimum happiness = %d (%s)\n", nh, student[ni].name); printf("maximum happiness = %d (%s)\n", mh, student[mi].name); printf("happiness distrib ="); for (j=nh;j<=mh&&j<20;++j) printf(" %d",hd[j]); printf("\n"); } printf("number of repeats = %d \n", nr); printf("number non assign = %d \n", nocl); printf("number non prereq = %d \n", nopre); printf("number overfilled = %d \n", ovful); } ++rep; if (nr==0 && th >= hap) // hap is max happiness so far { if (th==hap) ++ans; hap = th; if (rep > 4 || ans > 4) break; rep = 0; } if (n > swlim) break; if (debug) printf("\nSwapping.... pass %d\n",n); SearchAndSwap(); } } //---------------------------------------------------------------------------- // write the completed schedule void WriteOutput(char *filename) { int t,c,i,p; int nc=0, tc=0; int th=0, ov=0, mp=0; FILE *f; if (!strcmp(filename,"stdout")) f = stdout; else f = fopen(filename, "w"); if (export) { for (i=0; i1) printf(" (%d) %d", student[i].happiness, student[i].num_enroll); printf("\n"); th += student[i].happiness; for (p=0; p 1 || period[p].prec[c] && student[i].enroll[period[p].prep[c]]!= period[p].prec[c] && ++mp) ? "*\t" : "\t"); if (debug>1) printf("(%d) ", student[i].pref[t]); printf("%s", period[p].name); printf("\t%s", topic[t].name); if (period[p].room[c]) printf("\t%d", period[p].room[c]); printf("\n"); } } printf("\n"); } printf("\n======================================\n"); printf(" CLASSES LIST\n"); printf("======================================\n\n"); for (p=0; p 1 && ++tc ? "*\t" : "\t"); if (debug>1) printf("(%d) ",student[i].pref[t]); printf("%s\n", student[i].name); } printf("\n"); } } } if (debug) { printf("\nNumber of missing assignments: %d\n", nc); printf( "Number of repeated topics: %d\n", (tc + 1)/2); printf( "Number of overfilled classes: %d\n", ov); printf( "Number of missed prereqs: %d\n", mp); if (debug>1) printf( "Total happiness: %d\n", th); if (debug>1) printf( "Average happiness: %g\n", (double)th/num_student); } if (strcmp(filename,"stdout")) fclose(f); } //---------------------------------------------------------------------------- int main(int argc, char* argv[]) { int i,r; char *fn="ypSched.dat"; for (i=1; i